Jelajahi Sumber

Do not use Python code to change format of data sent

jherve 1 tahun lalu
induk
melakukan
562df99cc9

+ 2 - 105
native/src/job_search/job_storage.py

@@ -5,7 +5,6 @@ from urllib.parse import urlparse, ParseResult
 from dataclasses import dataclass, field, asdict
 from dataclasses import dataclass, field, asdict
 from enum import Enum
 from enum import Enum
 from datetime import date, datetime
 from datetime import date, datetime
-from email.utils import parsedate_to_datetime
 
 
 
 
 class JobOfferOrigin(Enum):
 class JobOfferOrigin(Enum):
@@ -39,108 +38,6 @@ class Flexibility(Enum):
     FULL_REMOTE = "full_remote"
     FULL_REMOTE = "full_remote"
 
 
 
 
-def convert_to_parse_result(url):
-    if isinstance(url, str):
-        return urlparse(url)
-    elif isinstance(url, ParseResult):
-        return url
-
-def convert_to_bool(s: str) -> bool:
-    return s == "true" or s == "yes" or s == "1"
-
-
-@dataclass
-class JobOffer:
-    id: str
-    url: str = field(repr=False)
-    title: str
-    company: str
-    origin: JobOfferOrigin
-    location: str
-    application_process: ApplicationProcess | None = None
-    company_url: str = ""
-    description: str = ""
-    company_kind: CompanyKind | None = None
-    company_domain: str = ""
-    comment: str = ""
-    tags: list[str] = field(default_factory=list)
-    skills: list[str] = field(default_factory=list)
-    publication_date: date = None
-    xp_required: int | None = None
-    first_seen_date: datetime | None = None
-    application_considered: bool | None = None
-    application_date: date | None = None
-    application_rejection_date: date | None = None
-    contract_type: ContractType | None = ContractType.CDI
-    flexibility: Flexibility | None = None
-    alternate_url: str = None
-    _url: ParseResult = field(init=False, repr=False)
-    _company_url: ParseResult = field(init=False, repr=False)
-    _alternate_url: ParseResult = field(init=False, repr=False, default=None)
-
-    def __post_init__(self):
-        self._url = convert_to_parse_result(self.url)
-        self.url = self._url.geturl()
-
-        self._company_url = convert_to_parse_result(self.company_url)
-        self.company_url = self._company_url.geturl()
-
-        if self.alternate_url:
-            self._alternate_url = convert_to_parse_result(self.alternate_url)
-            self.alternate_url = self._alternate_url.geturl()
-
-    def to_storage(self):
-        return {
-            k: v
-            for k, v in asdict(self).items()
-            if k not in ["_url", "_company_url", "_alternate_url"]
-        }
-
-    @staticmethod
-    def from_storage(dict: dict):
-        for field, converter in [
-            ("origin", JobOfferOrigin),
-            ("application_process", ApplicationProcess),
-            ("company_kind", CompanyKind),
-            ("contract_type", ContractType),
-            ("flexibility", Flexibility),
-            ("xp_required", int),
-            ("first_seen_date", parsedate_to_datetime),
-            ("publication_date", date.fromisoformat),
-            ("application_considered", convert_to_bool),
-            ("application_date", date.fromisoformat),
-            ("application_rejection_date", date.fromisoformat),
-        ]:
-            if field in dict:
-                dict[field] = converter(dict[field])
-
-        # For now we simply ignore application-related fields
-        # read from the storage.
-        for k in [
-            "application_first_seen_date",
-            "application_first_response_date",
-            "application_cv_version",
-            "application_appointments",
-            "application_message",
-            "application_questions",
-            "application_url",
-            "application_contacts",
-            "application_text",
-        ]:
-            try:
-                del dict[k]
-            except KeyError:
-                pass
-
-        return JobOffer(**dict)
-
-
-def remove_whitespace(s):
-    s = re.sub(r"[^\w\s]", "", s)
-    s = re.sub(r"\s+", "_", s)
-    return s
-
-
 @dataclass
 @dataclass
 class JobStorage:
 class JobStorage:
     base_dir: Path
     base_dir: Path
@@ -175,8 +72,8 @@ class JobStorage:
                 f.write("%type: first_seen_date date\n")
                 f.write("%type: first_seen_date date\n")
                 f.write("%auto: first_seen_date\n")
                 f.write("%auto: first_seen_date\n")
 
 
-    def read_all(self) -> dict[str, JobOffer]:
-        return {r["id"]: JobOffer.from_storage(r) for r in self.select_all("job_offer")}
+    def read_all(self) -> dict[str, dict]:
+        return {r["id"]: r for r in self.select_all("job_offer")}
 
 
     def insert_record(self, type_, fields):
     def insert_record(self, type_, fields):
         cmd_args = [
         cmd_args = [

+ 1 - 2
native/src/job_search/messages.py

@@ -4,7 +4,6 @@ from dataclasses import dataclass, asdict
 from enum import Enum
 from enum import Enum
 from typing import Optional, Any
 from typing import Optional, Any
 from job_search.job_storage import (
 from job_search.job_storage import (
-    JobOffer,
     ApplicationProcess,
     ApplicationProcess,
     JobOfferOrigin,
     JobOfferOrigin,
     Flexibility,
     Flexibility,
@@ -87,7 +86,7 @@ class InitialConfigurationMessage(BackgroundScriptMessage):
 
 
 @dataclass
 @dataclass
 class JobOfferListMessage(NativeMessage):
 class JobOfferListMessage(NativeMessage):
-    job_offers: list[JobOffer]
+    job_offers: list[dict]
 
 
     def serialize(self):
     def serialize(self):
         return {"tag": "NativeMessageJobOfferList", "values": [self.job_offers]}
         return {"tag": "NativeMessageJobOfferList", "values": [self.job_offers]}

+ 26 - 13
native/tests/test_storage.py

@@ -1,14 +1,16 @@
 import pytest
 import pytest
 from datetime import date, datetime
 from datetime import date, datetime
+from email.utils import parsedate_to_datetime
 from job_search.job_storage import (
 from job_search.job_storage import (
     JobStorage,
     JobStorage,
-    JobOffer,
     JobOfferOrigin,
     JobOfferOrigin,
     ApplicationProcess,
     ApplicationProcess,
     CompanyKind,
     CompanyKind,
     ContractType,
     ContractType,
     Flexibility,
     Flexibility,
 )
 )
+from job_search.read_write import EnhancedJSONEncoder
+import json
 
 
 
 
 @pytest.fixture(params=[JobStorage])
 @pytest.fixture(params=[JobStorage])
@@ -18,7 +20,7 @@ def job_storage(request, tmp_path):
 
 
 @pytest.fixture(
 @pytest.fixture(
     params=[
     params=[
-        JobOffer(
+        dict(
             id="linked_in_3755217595",
             id="linked_in_3755217595",
             url="https://www.linkedin.com/jobs/view/3755217595",
             url="https://www.linkedin.com/jobs/view/3755217595",
             title="Job title",
             title="Job title",
@@ -30,7 +32,7 @@ def job_storage(request, tmp_path):
             company_url="https://www.linkedin.com/company/the-company/life",
             company_url="https://www.linkedin.com/company/the-company/life",
             publication_date=date.today(),
             publication_date=date.today(),
         ),
         ),
-        JobOffer(
+        dict(
             id="linked_in_3755217595",
             id="linked_in_3755217595",
             url="https://www.linkedin.com/jobs/view/3755217595",
             url="https://www.linkedin.com/jobs/view/3755217595",
             title="Job title",
             title="Job title",
@@ -53,7 +55,7 @@ def job_storage(request, tmp_path):
             application_date=date.today(),
             application_date=date.today(),
             application_rejection_date=date.today(),
             application_rejection_date=date.today(),
         ),
         ),
-        JobOffer(
+        dict(
             id="linked_in_3755217595",
             id="linked_in_3755217595",
             url="https://www.linkedin.com/jobs/view/3755217595",
             url="https://www.linkedin.com/jobs/view/3755217595",
             title="Job title",
             title="Job title",
@@ -81,23 +83,34 @@ class TestJobStorage:
         assert job_storage.read_all() == {}
         assert job_storage.read_all() == {}
 
 
     def test_job_addition(self, job_storage, linked_in_job_offer):
     def test_job_addition(self, job_storage, linked_in_job_offer):
-        job_storage.insert_record("job_offer", linked_in_job_offer.to_storage())
+        job_storage.insert_record("job_offer", linked_in_job_offer)
 
 
         all_items = job_storage.read_all().items()
         all_items = job_storage.read_all().items()
         assert len(all_items) == 1
         assert len(all_items) == 1
 
 
         [(id, stored_job)] = all_items
         [(id, stored_job)] = all_items
-        assert isinstance(stored_job.first_seen_date, datetime)
-        assert id == stored_job.id
+        # Check that a "first seen date" field has been set, that reads as a datetime
+        assert isinstance(parsedate_to_datetime(stored_job["first_seen_date"]), datetime)
+        assert id == stored_job["id"]
+    
+        # Job offers should look as if encoded in JSON and then decoded ...
+        expected_read = json.loads(json.dumps(linked_in_job_offer, cls=EnhancedJSONEncoder))
+        # ... with some fields added
+        expected_read["skills"] = expected_read["skills"] if "skills" in expected_read else []
+        expected_read["tags"] = expected_read["tags"] if "tags" in expected_read else []
+        # ... and the integers won't be properly readback
+        if "xp_required" in expected_read:
+            expected_read["xp_required"] = str(expected_read["xp_required"])
 
 
-        # Reset the first_seen_date to None, for comparison with the non-stored version
-        stored_job.first_seen_date = None
-        assert stored_job == linked_in_job_offer
+        # Remove automatically-added first_seen_date for comparison with the non-stored version
+        del stored_job["first_seen_date"]
+
+        assert stored_job == expected_read
 
 
     def test_job_duplicate_addition(self, job_storage, linked_in_job_offer):
     def test_job_duplicate_addition(self, job_storage, linked_in_job_offer):
-        job_storage.insert_record("job_offer", linked_in_job_offer.to_storage())
+        job_storage.insert_record("job_offer", linked_in_job_offer)
 
 
         with pytest.raises(FileExistsError) as excinfo:
         with pytest.raises(FileExistsError) as excinfo:
-            job_storage.insert_record("job_offer", linked_in_job_offer.to_storage())
+            job_storage.insert_record("job_offer", linked_in_job_offer)
 
 
-        assert linked_in_job_offer.id in str(excinfo.value)
+        assert linked_in_job_offer["id"] in str(excinfo.value)

+ 18 - 2
src/NativeMessage.purs

@@ -7,8 +7,9 @@ import Browser.WebExt.Message (Message, mkMessage, unwrapMessage)
 import Browser.WebExt.Port (Port, onDisconnectAddListener, onMessageAddListener)
 import Browser.WebExt.Port (Port, onDisconnectAddListener, onMessageAddListener)
 import Browser.WebExt.Port as Port
 import Browser.WebExt.Port as Port
 import Browser.WebExt.Runtime (Application, connectNative)
 import Browser.WebExt.Runtime (Application, connectNative)
+import Control.Alt ((<|>))
 import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError(..), printJsonDecodeError)
 import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError(..), printJsonDecodeError)
-import Data.Argonaut.Decode.Decoders (decodeString)
+import Data.Argonaut.Decode.Decoders (decodeBoolean, decodeString)
 import Data.Argonaut.Decode.Generic (genericDecodeJson)
 import Data.Argonaut.Decode.Generic (genericDecodeJson)
 import Data.Argonaut.Encode (class EncodeJson, encodeJson)
 import Data.Argonaut.Encode (class EncodeJson, encodeJson)
 import Data.Argonaut.Encode.Generic (genericEncodeJson)
 import Data.Argonaut.Encode.Generic (genericEncodeJson)
@@ -55,6 +56,21 @@ instance DecodeJson ApplicationProcess where
     Right "spurious" -> Right ApplicationProcessSpurious
     Right "spurious" -> Right ApplicationProcessSpurious
     _ -> Left $ UnexpectedValue json
     _ -> Left $ UnexpectedValue json
 
 
+newtype BooleanStr = BooleanStr Boolean
+derive instance Generic BooleanStr _
+instance Show BooleanStr where show = genericShow
+instance EncodeJson BooleanStr where encodeJson (BooleanStr bool) = encodeJson bool
+
+instance DecodeJson BooleanStr where
+  decodeJson json = decodedAsBoolean <|> decodedAsString
+    where
+      decodedAsBoolean = map (\b -> BooleanStr b) $ decodeBoolean json
+      decodedAsString =
+        case decodeString json of
+          Right "true" -> Right (BooleanStr true)
+          Right "false" -> Right (BooleanStr false)
+          _ -> Left $ UnexpectedValue json
+
 type NativePythonJobOffer = {
 type NativePythonJobOffer = {
   id :: String,
   id :: String,
   origin :: String,
   origin :: String,
@@ -68,7 +84,7 @@ type NativePythonJobOffer = {
   flexibility :: Maybe JobFlexibility,
   flexibility :: Maybe JobFlexibility,
   comment :: Maybe String,
   comment :: Maybe String,
   application_process :: Maybe ApplicationProcess,
   application_process :: Maybe ApplicationProcess,
-  application_considered :: Maybe Boolean,
+  application_considered :: Maybe BooleanStr,
   application_date :: Maybe String,
   application_date :: Maybe String,
   application_rejection_date :: Maybe String
   application_rejection_date :: Maybe String
 }
 }