jobhunter-cli 0.0.post1.dev85__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jobhunter/__init__.py +0 -0
- jobhunter/__main__.py +4 -0
- jobhunter/_version.py +24 -0
- jobhunter/core/__init__.py +0 -0
- jobhunter/core/filters.py +110 -0
- jobhunter/core/http.py +57 -0
- jobhunter/core/logger.py +54 -0
- jobhunter/core/paths.py +124 -0
- jobhunter/core/settings.py +123 -0
- jobhunter/core/textutil.py +33 -0
- jobhunter/main.py +101 -0
- jobhunter/models.py +81 -0
- jobhunter/pipeline/__init__.py +0 -0
- jobhunter/pipeline/evaluate.py +284 -0
- jobhunter/pipeline/fetch.py +233 -0
- jobhunter/pipeline/send.py +191 -0
- jobhunter/sources/__init__.py +21 -0
- jobhunter/sources/arbeitnow.py +77 -0
- jobhunter/sources/ashby.py +75 -0
- jobhunter/sources/base.py +108 -0
- jobhunter/sources/greenhouse.py +70 -0
- jobhunter/sources/lever.py +85 -0
- jobhunter/sources/personio.py +124 -0
- jobhunter/sources/recruitee.py +80 -0
- jobhunter/sources/remoteok.py +69 -0
- jobhunter/sources/remotive.py +61 -0
- jobhunter/sources/workable.py +94 -0
- jobhunter/tools/__init__.py +0 -0
- jobhunter/tools/discover.py +275 -0
- jobhunter/tools/init.py +74 -0
- jobhunter/tools/prompt.py +106 -0
- jobhunter/tools/prune.py +124 -0
- jobhunter_cli-0.0.post1.dev85.dist-info/METADATA +136 -0
- jobhunter_cli-0.0.post1.dev85.dist-info/RECORD +37 -0
- jobhunter_cli-0.0.post1.dev85.dist-info/WHEEL +4 -0
- jobhunter_cli-0.0.post1.dev85.dist-info/entry_points.txt +2 -0
- jobhunter_cli-0.0.post1.dev85.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from jobhunter.core.http import fetch_url
|
|
8
|
+
from jobhunter.core.logger import console
|
|
9
|
+
from jobhunter.core.textutil import clean_html, parse_date
|
|
10
|
+
from jobhunter.models import Job
|
|
11
|
+
from jobhunter.sources.base import BaseSource
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Source(BaseSource):
|
|
17
|
+
name = "personio"
|
|
18
|
+
domains = ["personio.de", "personio.com"]
|
|
19
|
+
url_patterns = [
|
|
20
|
+
re.compile(r"(?://|https?://)(?!www\.)(?P<slug>[a-zA-Z0-9-]+)\.(?:jobs\.)?personio\.(?:de|com|es|fr|it)"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def fetch(self, slug: str = "", skip_parsing: bool = False, **kwargs) -> list[Job]:
|
|
24
|
+
"""Fetches job listings from a specific company's Personio XML feed."""
|
|
25
|
+
if not slug:
|
|
26
|
+
raise ValueError(f"A company slug must be provided to fetch {self.name} jobs.")
|
|
27
|
+
|
|
28
|
+
response = None
|
|
29
|
+
base_url = ""
|
|
30
|
+
headers = {"Accept": "application/xml,text/xml,*/*"}
|
|
31
|
+
last_error = None
|
|
32
|
+
|
|
33
|
+
for domain in ("de", "com"):
|
|
34
|
+
base_url = f"https://{slug}.jobs.personio.{domain}"
|
|
35
|
+
url = f"{base_url}/xml?language=en"
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
response = fetch_url(url, headers=headers)
|
|
39
|
+
break
|
|
40
|
+
except httpx.HTTPStatusError as e:
|
|
41
|
+
last_error = e
|
|
42
|
+
if e.response.status_code == 404:
|
|
43
|
+
continue
|
|
44
|
+
raise
|
|
45
|
+
except httpx.RequestError as e:
|
|
46
|
+
last_error = e
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if not response:
|
|
50
|
+
if last_error:
|
|
51
|
+
raise last_error
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
if skip_parsing:
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
root = ET.fromstring(response.text)
|
|
59
|
+
except ET.ParseError as exc:
|
|
60
|
+
logger.error(f"[ {self.name} ] Failed to parse XML for '{slug}': {exc}")
|
|
61
|
+
console.print(f" [red]✖ [ {self.name} ] Failed to parse XML for '{slug}'[/red]")
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
jobs: list[Job] = []
|
|
65
|
+
|
|
66
|
+
for position in root.iter("position"):
|
|
67
|
+
|
|
68
|
+
def field(tag: str, pos=position) -> str:
|
|
69
|
+
return (pos.findtext(tag) or "").strip()
|
|
70
|
+
|
|
71
|
+
job_id = str(field("id") or "")
|
|
72
|
+
title = str(field("name") or "")
|
|
73
|
+
job_url = str(f"{base_url}/job/{job_id}" if job_id else "")
|
|
74
|
+
|
|
75
|
+
if not all([job_id, title, job_url]):
|
|
76
|
+
logger.warning(f"[ {self.name} ] Skipping item with missing required fields: {position.attrib}")
|
|
77
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping item with missing required fields.[/yellow]")
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
offices = [field("office")] + [(o.text or "").strip() for o in position.findall("additionalOffices/office")]
|
|
82
|
+
location = ", ".join(dict.fromkeys(o for o in offices if o))
|
|
83
|
+
|
|
84
|
+
sections = []
|
|
85
|
+
for block in position.findall("jobDescriptions/jobDescription"):
|
|
86
|
+
name = (block.findtext("name") or "").strip()
|
|
87
|
+
value = clean_html(block.findtext("value"))
|
|
88
|
+
if value:
|
|
89
|
+
sections.append(f"{name}\n{value}" if name else value)
|
|
90
|
+
|
|
91
|
+
facts = [
|
|
92
|
+
f"{label}: {field(tag)}"
|
|
93
|
+
for label, tag in (
|
|
94
|
+
("Seniority", "seniority"),
|
|
95
|
+
("Years of experience", "yearsOfExperience"),
|
|
96
|
+
("Employment", "employmentType"),
|
|
97
|
+
("Schedule", "schedule"),
|
|
98
|
+
)
|
|
99
|
+
if field(tag)
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
description = ("\n".join(facts) + "\n\n" if facts else "") + "\n\n".join(sections)
|
|
103
|
+
|
|
104
|
+
jobs.append(
|
|
105
|
+
Job(
|
|
106
|
+
source=self.name,
|
|
107
|
+
board=slug,
|
|
108
|
+
company=field("subcompany") or slug.replace("-", " ").title(),
|
|
109
|
+
job_id=job_id,
|
|
110
|
+
title=title,
|
|
111
|
+
url=job_url,
|
|
112
|
+
location=location,
|
|
113
|
+
remote=None,
|
|
114
|
+
posted=parse_date(field("createdAt")),
|
|
115
|
+
description=description,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.exception(f"[ {self.name} ] Error parsing job '{job_id}' for '{slug}': {e}")
|
|
121
|
+
console.print(f" [red]✖ [ {self.name} ] Error parsing job '{job_id}'[/red]")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
return jobs
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from jobhunter.core.http import fetch_url
|
|
5
|
+
from jobhunter.core.logger import console
|
|
6
|
+
from jobhunter.core.textutil import clean_html, parse_date
|
|
7
|
+
from jobhunter.models import Job
|
|
8
|
+
from jobhunter.sources.base import BaseSource
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Source(BaseSource):
|
|
14
|
+
name = "recruitee"
|
|
15
|
+
domains = ["recruitee.com"]
|
|
16
|
+
url_patterns = [
|
|
17
|
+
re.compile(r"(?://|https?://)?(?:www\.)?(?P<slug>[a-zA-Z0-9-]+)\.recruitee\.com"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
def fetch(self, slug: str = "", skip_parsing: bool = False, **kwargs) -> list[Job]:
|
|
21
|
+
"""Fetches job listings from a specific company's Recruitee API."""
|
|
22
|
+
if not slug:
|
|
23
|
+
raise ValueError(f"A company slug must be provided to fetch {self.name} jobs.")
|
|
24
|
+
|
|
25
|
+
url = f"https://{slug}.recruitee.com/api/offers/"
|
|
26
|
+
data = fetch_url(url, expected_type=dict)
|
|
27
|
+
|
|
28
|
+
if skip_parsing:
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
jobs: list[Job] = []
|
|
32
|
+
|
|
33
|
+
for item in data.get("offers", []):
|
|
34
|
+
if not isinstance(item, dict):
|
|
35
|
+
logger.warning(f"[ {self.name} ] Skipping unexpected item format: {type(item).__name__}")
|
|
36
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping unexpected item format...[/yellow]")
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
job_id = str(item.get("id") or item.get("guid") or item.get("slug") or "")
|
|
40
|
+
title = str(item.get("title") or "")
|
|
41
|
+
job_url = str(item.get("careers_url") or item.get("url") or "")
|
|
42
|
+
|
|
43
|
+
if not all([job_id, title, job_url]):
|
|
44
|
+
logger.warning(f"[ {self.name} ] Skipping item with missing required fields: {item}")
|
|
45
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping item with missing required fields.[/yellow]")
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
location = item.get("location") or ", ".join(x for x in (item.get("city"), item.get("country")) if x)
|
|
50
|
+
|
|
51
|
+
description = "\n\n".join(
|
|
52
|
+
x
|
|
53
|
+
for x in (
|
|
54
|
+
clean_html(item.get("description")),
|
|
55
|
+
clean_html(item.get("requirements")),
|
|
56
|
+
)
|
|
57
|
+
if x
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
jobs.append(
|
|
61
|
+
Job(
|
|
62
|
+
source=self.name,
|
|
63
|
+
board=slug,
|
|
64
|
+
company=item.get("company_name") or slug.replace("-", " ").title(),
|
|
65
|
+
job_id=job_id,
|
|
66
|
+
title=title,
|
|
67
|
+
url=job_url,
|
|
68
|
+
location=location or "",
|
|
69
|
+
remote=True if item.get("remote") else None,
|
|
70
|
+
posted=parse_date(item.get("published_at") or item.get("created_at")),
|
|
71
|
+
description=description,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.exception(f"[ {self.name} ] Error parsing job '{job_id}' for '{slug}': {e}")
|
|
77
|
+
console.print(f" [red]✖ [ {self.name} ] Error parsing job '{job_id}'[/red]")
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
return jobs
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from jobhunter.core.http import fetch_url
|
|
4
|
+
from jobhunter.core.logger import console
|
|
5
|
+
from jobhunter.core.textutil import clean_html, parse_date
|
|
6
|
+
from jobhunter.models import Job
|
|
7
|
+
from jobhunter.sources.base import BaseSource
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Source(BaseSource):
|
|
13
|
+
name = "remoteok"
|
|
14
|
+
domains = ["remoteok.com"]
|
|
15
|
+
is_aggregator = True
|
|
16
|
+
|
|
17
|
+
def fetch(self, slug: str = "", skip_parsing: bool = False, **kwargs) -> list[Job]:
|
|
18
|
+
"""Fetches job listings from a specific company's RemoteOK API."""
|
|
19
|
+
url = "https://remoteok.com/api"
|
|
20
|
+
data = fetch_url(url, expected_type=list)
|
|
21
|
+
jobs: list[Job] = []
|
|
22
|
+
|
|
23
|
+
for item in data:
|
|
24
|
+
if not isinstance(item, dict):
|
|
25
|
+
logger.warning(f"[ {self.name} ] Skipping unexpected item format: {type(item).__name__}")
|
|
26
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping unexpected item format...[/yellow]")
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if "legal" in item:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
job_id = str(item.get("id") or item.get("slug") or "")
|
|
33
|
+
title = str(item.get("position") or "")
|
|
34
|
+
job_url = str(item.get("url") or item.get("apply_url") or "")
|
|
35
|
+
|
|
36
|
+
if not all([job_id, title, job_url]):
|
|
37
|
+
logger.warning(f"[ {self.name} ] Skipping item with missing required fields: {item}")
|
|
38
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping item with missing required fields.[/yellow]")
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
raw_tags = item.get("tags") or []
|
|
43
|
+
tags = ", ".join(str(t) for t in raw_tags if t)
|
|
44
|
+
description = item.get("description", "")
|
|
45
|
+
|
|
46
|
+
if tags:
|
|
47
|
+
description = f"Tags: {tags}\n\n{description}"
|
|
48
|
+
|
|
49
|
+
jobs.append(
|
|
50
|
+
Job(
|
|
51
|
+
source=self.name,
|
|
52
|
+
board=self.name,
|
|
53
|
+
company=item.get("company") or "Unknown",
|
|
54
|
+
job_id=job_id,
|
|
55
|
+
title=title,
|
|
56
|
+
url=job_url,
|
|
57
|
+
location=item.get("location") or "Remote",
|
|
58
|
+
remote=True,
|
|
59
|
+
posted=parse_date(item.get("date")),
|
|
60
|
+
description=clean_html(description),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.exception(f"[ {self.name} ] Error parsing job '{job_id}': {e}")
|
|
66
|
+
console.print(f" [red]✖ [ {self.name} ] Error parsing job '{job_id}'[/red]")
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
return jobs
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from jobhunter.core.http import fetch_url
|
|
4
|
+
from jobhunter.core.logger import console
|
|
5
|
+
from jobhunter.core.textutil import clean_html, parse_date
|
|
6
|
+
from jobhunter.models import Job
|
|
7
|
+
from jobhunter.sources.base import BaseSource
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Source(BaseSource):
|
|
13
|
+
name = "remotive"
|
|
14
|
+
domains = ["remotive.com", "remotive.io"]
|
|
15
|
+
is_aggregator = True
|
|
16
|
+
|
|
17
|
+
def fetch(self, slug: str = "", skip_parsing: bool = False, **kwargs) -> list[Job]:
|
|
18
|
+
"""Fetches job listings from Remotive API."""
|
|
19
|
+
category = self.board_config.get("category", "software-dev") if isinstance(self.board_config, dict) else "software-dev"
|
|
20
|
+
|
|
21
|
+
url = f"https://remotive.com/api/remote-jobs?category={category}"
|
|
22
|
+
data = fetch_url(url, expected_type=dict)
|
|
23
|
+
jobs: list[Job] = []
|
|
24
|
+
|
|
25
|
+
for item in data.get("jobs", []):
|
|
26
|
+
if not isinstance(item, dict):
|
|
27
|
+
logger.warning(f"[ {self.name} ] Skipping unexpected item format: {type(item).__name__}")
|
|
28
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping unexpected item format...[/yellow]")
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
job_id = str(item.get("id") or "")
|
|
32
|
+
title = str(item.get("title") or "")
|
|
33
|
+
job_url = str(item.get("url") or "")
|
|
34
|
+
|
|
35
|
+
if not all([job_id, title, job_url]):
|
|
36
|
+
logger.warning(f"[ {self.name} ] Skipping item with missing required fields: {item}")
|
|
37
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping item with missing required fields.[/yellow]")
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
jobs.append(
|
|
42
|
+
Job(
|
|
43
|
+
source=self.name,
|
|
44
|
+
board=self.name,
|
|
45
|
+
company=item.get("company_name") or "Unknown",
|
|
46
|
+
job_id=job_id,
|
|
47
|
+
title=title,
|
|
48
|
+
url=job_url,
|
|
49
|
+
location=item.get("candidate_required_location") or "Remote",
|
|
50
|
+
remote=True,
|
|
51
|
+
posted=parse_date(item.get("publication_date")),
|
|
52
|
+
description=clean_html(item.get("description", "")),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.exception(f"[ {self.name} ] Error parsing job '{job_id}': {e}")
|
|
58
|
+
console.print(f" [red]✖ [ {self.name} ] Error parsing job '{job_id}'[/red]")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
return jobs
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from jobhunter.core.http import fetch_url
|
|
5
|
+
from jobhunter.core.logger import console
|
|
6
|
+
from jobhunter.core.textutil import clean_html, parse_date
|
|
7
|
+
from jobhunter.models import Job
|
|
8
|
+
from jobhunter.sources.base import BaseSource
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Source(BaseSource):
|
|
14
|
+
name = "workable"
|
|
15
|
+
domains = ["apply.workable.com"]
|
|
16
|
+
url_patterns = [
|
|
17
|
+
re.compile(r"apply\.workable\.com/(?P<slug>[^/?#]+)"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _parse_location(item: dict, location_obj: dict) -> str:
|
|
22
|
+
"""Extracts and formats location data from Workable's messy location fields."""
|
|
23
|
+
parts = [
|
|
24
|
+
item.get("city") or location_obj.get("city"),
|
|
25
|
+
item.get("state") or location_obj.get("region"),
|
|
26
|
+
item.get("country") or location_obj.get("country"),
|
|
27
|
+
]
|
|
28
|
+
text = ", ".join(p for p in parts if p)
|
|
29
|
+
|
|
30
|
+
if text:
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
names = []
|
|
34
|
+
|
|
35
|
+
for entry in item.get("locations") or []:
|
|
36
|
+
if isinstance(entry, dict):
|
|
37
|
+
names.append(", ".join(x for x in (entry.get("city"), entry.get("region"), entry.get("country")) if x))
|
|
38
|
+
|
|
39
|
+
return " / ".join(n for n in names if n)
|
|
40
|
+
|
|
41
|
+
def fetch(self, slug: str = "", skip_parsing: bool = False, **kwargs) -> list[Job]:
|
|
42
|
+
"""Fetches job listings from a specific company's Workable API."""
|
|
43
|
+
if not slug:
|
|
44
|
+
raise ValueError(f"A company slug must be provided to fetch {self.name} jobs.")
|
|
45
|
+
|
|
46
|
+
url = f"https://apply.workable.com/api/v1/widget/accounts/{slug}?details=true"
|
|
47
|
+
data = fetch_url(url, expected_type=dict)
|
|
48
|
+
|
|
49
|
+
if skip_parsing:
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
jobs: list[Job] = []
|
|
53
|
+
|
|
54
|
+
for item in data.get("jobs", []):
|
|
55
|
+
if not isinstance(item, dict):
|
|
56
|
+
logger.warning(f"[ {self.name} ] Skipping unexpected item format: {type(item).__name__}")
|
|
57
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping unexpected item format...[/yellow]")
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
job_id = str(item.get("shortcode") or item.get("id") or item.get("code") or "")
|
|
61
|
+
title = str(item.get("title") or "")
|
|
62
|
+
job_url = str(item.get("url") or item.get("shortlink") or item.get("application_url") or "")
|
|
63
|
+
|
|
64
|
+
if not all([job_id, title, job_url]):
|
|
65
|
+
logger.warning(f"[ {self.name} ] Skipping item with missing required fields: {item}")
|
|
66
|
+
console.print(f" [yellow]⚠ [ {self.name} ] Skipping item with missing required fields.[/yellow]")
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
loc_val = item.get("location")
|
|
71
|
+
location_obj = loc_val if isinstance(loc_val, dict) else {}
|
|
72
|
+
is_remote = bool(item.get("telecommuting")) or bool(location_obj.get("telecommuting"))
|
|
73
|
+
|
|
74
|
+
jobs.append(
|
|
75
|
+
Job(
|
|
76
|
+
source=self.name,
|
|
77
|
+
board=slug,
|
|
78
|
+
company=data.get("name") or slug.replace("-", " ").title(),
|
|
79
|
+
job_id=job_id,
|
|
80
|
+
title=title,
|
|
81
|
+
url=job_url,
|
|
82
|
+
location=self._parse_location(item, location_obj),
|
|
83
|
+
remote=True if is_remote else None,
|
|
84
|
+
posted=parse_date(item.get("published_on") or item.get("created_at")),
|
|
85
|
+
description=clean_html(item.get("description") or item.get("full_description") or ""),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.exception(f"[ {self.name} ] Error parsing job '{job_id}' for '{slug}': {e}")
|
|
91
|
+
console.print(f" [red]✖ [ {self.name} ] Error parsing job '{job_id}'[/red]")
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
return jobs
|
|
File without changes
|