talentro-commons 0.1.1__tar.gz

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.
Files changed (30) hide show
  1. talentro_commons-0.1.1/PKG-INFO +33 -0
  2. talentro_commons-0.1.1/README.md +17 -0
  3. talentro_commons-0.1.1/pyproject.toml +26 -0
  4. talentro_commons-0.1.1/src/talentro/__init__.py +0 -0
  5. talentro_commons-0.1.1/src/talentro/billing/__init__.py +0 -0
  6. talentro_commons-0.1.1/src/talentro/billing/dataclasses.py +0 -0
  7. talentro_commons-0.1.1/src/talentro/billing/models.py +26 -0
  8. talentro_commons-0.1.1/src/talentro/campaigns/__init__.py +0 -0
  9. talentro_commons-0.1.1/src/talentro/campaigns/dataclasses.py +22 -0
  10. talentro_commons-0.1.1/src/talentro/campaigns/models.py +80 -0
  11. talentro_commons-0.1.1/src/talentro/candidates/__init__.py +0 -0
  12. talentro_commons-0.1.1/src/talentro/candidates/dataclasses.py +37 -0
  13. talentro_commons-0.1.1/src/talentro/constants.py +28 -0
  14. talentro_commons-0.1.1/src/talentro/event.py +35 -0
  15. talentro_commons-0.1.1/src/talentro/exceptions.py +45 -0
  16. talentro_commons-0.1.1/src/talentro/general/__init__.py +0 -0
  17. talentro_commons-0.1.1/src/talentro/general/dataclasses.py +0 -0
  18. talentro_commons-0.1.1/src/talentro/general/models.py +64 -0
  19. talentro_commons-0.1.1/src/talentro/iam/__init__.py +0 -0
  20. talentro_commons-0.1.1/src/talentro/iam/dataclasses.py +0 -0
  21. talentro_commons-0.1.1/src/talentro/iam/models.py +19 -0
  22. talentro_commons-0.1.1/src/talentro/iam/types.py +12 -0
  23. talentro_commons-0.1.1/src/talentro/integrations/__init__.py +0 -0
  24. talentro_commons-0.1.1/src/talentro/integrations/dataclasses.py +34 -0
  25. talentro_commons-0.1.1/src/talentro/integrations/models.py +25 -0
  26. talentro_commons-0.1.1/src/talentro/util/__init__.py +0 -0
  27. talentro_commons-0.1.1/src/talentro/util/enum.py +6 -0
  28. talentro_commons-0.1.1/src/talentro/vacancies/__init__.py +0 -0
  29. talentro_commons-0.1.1/src/talentro/vacancies/dataclasses.py +172 -0
  30. talentro_commons-0.1.1/src/talentro/vacancies/models.py +81 -0
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.3
2
+ Name: talentro-commons
3
+ Version: 0.1.1
4
+ Summary: This package contains all globally used code, services, models and data structures for Talentro
5
+ License: Proprietary
6
+ Author: Emiel van Essen
7
+ Author-email: emiel@marksmen.nl
8
+ Requires-Python: >=3.13
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: fastapi (>=0.116.0,<0.117.0)
13
+ Requires-Dist: sqlalchemy (>=2.0.38,<3.0.0)
14
+ Requires-Dist: sqlmodel (>=0.0.24,<0.0.25)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Talentro definitions
18
+
19
+ This package contains all models and data structures for Talentro
20
+ It is exclusively meant for the Talentro ecosystem.
21
+
22
+ ## Initial run
23
+
24
+ - Run `poetry self update && poetry self add keyrings.google-artifactregistry-auth`
25
+ - Run `poetry config repositories.gcp https://europe-west4-python.pkg.dev/talentro-459113/talentro-python`
26
+
27
+ ## How to create a new version
28
+
29
+ - Make changes in the code, like editing the models
30
+ - Bump the version number to desired version in `pyproject.toml` using the `major.minor.fix` format
31
+ - run `poetry publish --build --repository gcp`
32
+
33
+ Now a new version is uploaded to pypi and you can install it after a minute in the other projects.
@@ -0,0 +1,17 @@
1
+ # Talentro definitions
2
+
3
+ This package contains all models and data structures for Talentro
4
+ It is exclusively meant for the Talentro ecosystem.
5
+
6
+ ## Initial run
7
+
8
+ - Run `poetry self update && poetry self add keyrings.google-artifactregistry-auth`
9
+ - Run `poetry config repositories.gcp https://europe-west4-python.pkg.dev/talentro-459113/talentro-python`
10
+
11
+ ## How to create a new version
12
+
13
+ - Make changes in the code, like editing the models
14
+ - Bump the version number to desired version in `pyproject.toml` using the `major.minor.fix` format
15
+ - run `poetry publish --build --repository gcp`
16
+
17
+ Now a new version is uploaded to pypi and you can install it after a minute in the other projects.
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "talentro-commons"
3
+ version = "0.1.1"
4
+ description = "This package contains all globally used code, services, models and data structures for Talentro"
5
+ authors = [
6
+ { name = "Emiel van Essen", email = "emiel@marksmen.nl"}
7
+ ]
8
+ license = { text = "Proprietary" }
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "sqlalchemy (>=2.0.38,<3.0.0)",
13
+ "sqlmodel (>=0.0.24,<0.0.25)",
14
+ "fastapi (>=0.116.0,<0.117.0)"
15
+ ]
16
+
17
+
18
+ [tool.poetry]
19
+ packages = [
20
+ {include = "talentro", from = "src"}
21
+ ]
22
+
23
+
24
+ [build-system]
25
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
26
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,26 @@
1
+ from uuid import UUID
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Optional
5
+
6
+ from sqlmodel import SQLModel, Field
7
+
8
+ from ..general.models import BillingOrganizationModel
9
+
10
+
11
+ class BillingEvent(SQLModel, table=True):
12
+ id: int = Field(
13
+ primary_key=True,
14
+ )
15
+
16
+ organization: UUID = Field(index=True)
17
+ event_time: Optional[datetime] = Field(
18
+ default_factory=lambda: datetime.now(timezone.utc),
19
+ nullable=False,
20
+ )
21
+ sku: str = Field(nullable=False)
22
+ count: int = Field(nullable=False, default=1)
23
+
24
+
25
+ class BillingProfile(BillingOrganizationModel, table=True):
26
+ stripe_customer_id: str = Field(nullable=False)
@@ -0,0 +1,22 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from uuid import UUID
4
+
5
+ from ..campaigns.models import ChannelType, CampaignGoal
6
+ from ..general.models import BaseModel
7
+ from ..integrations.dataclasses import Integration
8
+
9
+
10
+ class Campaign(BaseModel):
11
+ id: UUID
12
+ created_at: datetime
13
+ updated_at: Optional[datetime]
14
+ organization: UUID
15
+ name: str
16
+ external_id: Optional[str]
17
+ status: str
18
+ last_sync_date: datetime
19
+ ad_count: int
20
+ channel: Integration
21
+ channel_type: ChannelType
22
+ campaign_goal: Optional[CampaignGoal]
@@ -0,0 +1,80 @@
1
+ from uuid import UUID
2
+ import enum
3
+
4
+ from datetime import datetime
5
+ from enum import StrEnum
6
+ from typing import Optional
7
+
8
+ from sqlalchemy import Column, JSON, Enum
9
+ from sqlmodel import Field
10
+
11
+ from ..general.models import CampaignsOrganizationModel
12
+
13
+
14
+ class ChannelType(StrEnum):
15
+ JOB_BOARD = 'job-board'
16
+ SOCIAL = 'social'
17
+
18
+
19
+ class CampaignGoal(enum.Enum):
20
+ REACH = 'reach'
21
+ TRAFFIC = 'traffic'
22
+ CONVERSION = 'conversion'
23
+ LEADS = 'leads'
24
+
25
+
26
+ class Campaign(CampaignsOrganizationModel, table=True):
27
+ name: str = Field(index=True)
28
+ external_id: Optional[str] = Field(index=True)
29
+ status: str = Field(index=True)
30
+ last_sync_date: Optional[datetime] = Field()
31
+ ad_count: int = Field(default=0)
32
+ channel_id: UUID = Field(index=True)
33
+ channel_type: ChannelType = Field(sa_column=Column(Enum(ChannelType)))
34
+ campaign_goal: Optional[CampaignGoal] = Field(sa_column=Column(Enum(CampaignGoal)))
35
+
36
+
37
+ class VacancySelection(CampaignsOrganizationModel):
38
+ campaign_id: UUID = Field(foreign_key="campaign.id")
39
+ feed_id: UUID = Field(index=True)
40
+ selection_criteria: dict = Field(sa_column=Column(JSON))
41
+
42
+
43
+ class AdSet(CampaignsOrganizationModel, table=True):
44
+ name: str = Field(index=True)
45
+ external_id: Optional[str] = Field(index=True)
46
+ campaign_id: UUID = Field(foreign_key="campaign.id")
47
+ platforms: list = Field(sa_column=Column(JSON))
48
+ ad_types: list = Field(sa_column=Column(JSON))
49
+ settings: dict = Field(sa_column=Column(JSON))
50
+
51
+
52
+ class TargetLocation(CampaignsOrganizationModel, table=True):
53
+ ad_set: UUID = Field(foreign_key="adset.id")
54
+ address: str = Field(index=True)
55
+ distance: int = Field(index=True)
56
+
57
+
58
+ class TargetAudience(CampaignsOrganizationModel, table=True):
59
+ ad_set: UUID = Field(foreign_key="adset.id")
60
+ age_min: int = Field(index=True, default=18)
61
+ age_max: int = Field(index=True, default=150)
62
+ interests: list = Field(sa_column=Column(JSON))
63
+ languages: list = Field(sa_column=Column(JSON))
64
+
65
+
66
+ class Ad(CampaignsOrganizationModel, table=True):
67
+ name: str = Field(index=True)
68
+ external_id: Optional[str] = Field(index=True)
69
+ campaign_id: UUID = Field(foreign_key="campaign.id")
70
+ ad_set_id: Optional[UUID] = Field(foreign_key="adset.id")
71
+ vacancy_id: Optional[UUID] = Field()
72
+ lead_form: Optional[UUID] = Field(foreign_key="leadform.id")
73
+ primary_text: str = Field()
74
+ title: str = Field()
75
+ description: Optional[str] = Field()
76
+ conversion_goal: Optional[str] = Field()
77
+
78
+
79
+ class LeadForm(CampaignsOrganizationModel, table=True):
80
+ title: str = Field()
@@ -0,0 +1,37 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class CandidateData(BaseModel):
9
+ email: str
10
+ first_name: str
11
+ last_name: str
12
+ phone_number: str
13
+ hashed_email: str
14
+ cv: str
15
+ motivation_letter: str
16
+ linked_in: str
17
+
18
+
19
+ class Candidate(CandidateData):
20
+ id: UUID
21
+ created_at: datetime
22
+ updated_at: Optional[datetime]
23
+ organization: UUID
24
+
25
+
26
+ class ApplicationData(BaseModel):
27
+ status: str
28
+ source: str
29
+ candidate_id: str
30
+ vacancy_id: str
31
+
32
+
33
+ class Application(ApplicationData):
34
+ id: UUID
35
+ created_at: datetime
36
+ updated_at: Optional[datetime]
37
+ organization: UUID
@@ -0,0 +1,28 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class ErrorCode(StrEnum):
5
+ UNKNOWN_ERROR = "UNKNOWN_ERROR"
6
+ NOT_AUTHENTICATED = "NOT_AUTHENTICATED"
7
+ NOT_FOUND = "NOT_FOUND"
8
+ BAD_REQUEST = "BAD_REQUEST"
9
+ ARGUMENT_ERROR = "ARGUMENT_ERROR"
10
+ NO_MEMBERSHIP = "NO_MEMBERSHIP"
11
+ PERMISSION_DENIED = "PERMISSION_DENIED"
12
+ ALREADY_EXISTS = "ALREADY_EXISTS"
13
+
14
+
15
+ class DisplayMessage(StrEnum):
16
+ UNKNOWN_ERROR = "errors.unknownError"
17
+ NOT_AUTHENTICATED = "errors.notAuthenticated"
18
+ NOT_FOUND = "errors.notFound"
19
+ BAD_REQUEST = "errors.badRequest"
20
+ ARGUMENT_ERROR = "errors.argumentError"
21
+ PERMISSION_DENIED = "errors.noPermission"
22
+ NO_MEMBERSHIP = "errors.noMembership"
23
+ ALREADY_EXISTS = "errors.alreadyExists"
24
+
25
+
26
+ class SKU(StrEnum):
27
+ SYNCED_VACANCY = 'synced_vacancy'
28
+
@@ -0,0 +1,35 @@
1
+ import json
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from enum import StrEnum
5
+
6
+
7
+ class Queue(StrEnum):
8
+ integrations = "integrations"
9
+ candidates = "candidates"
10
+ vacancies = "vacancies"
11
+ strategy = "strategy"
12
+ campaigns = "campaigns"
13
+ insights = "insights"
14
+ billing = "billing"
15
+ iam = "iam"
16
+ notifications = "notifications"
17
+ live_updates = "live-updates"
18
+
19
+
20
+ @dataclass
21
+ class Event:
22
+ event_type: str
23
+ payload: dict
24
+ organization_id: str | None = None
25
+ created_on: datetime = None
26
+
27
+ def encode(self) -> bytes:
28
+ self.created_on: datetime = datetime.now()
29
+ return json.dumps(self.__dict__, default=str).encode()
30
+
31
+
32
+ @dataclass
33
+ class Message:
34
+ event: Event
35
+ queue: Queue
@@ -0,0 +1,45 @@
1
+ from http import HTTPStatus
2
+ from typing import Any, Optional
3
+
4
+ from fastapi import HTTPException
5
+
6
+
7
+ class APIException(HTTPException):
8
+ def __init__(
9
+ self,
10
+ status_code: HTTPStatus,
11
+ error_code: str,
12
+ message: str,
13
+ display_message: str,
14
+ headers: Optional[dict[str, Any]] = None
15
+ ) -> None:
16
+ detail = {
17
+ "error_code": error_code,
18
+ "message": message,
19
+ "display_message": display_message
20
+ }
21
+
22
+ super().__init__(
23
+ status_code=status_code.value,
24
+ detail=detail,
25
+ headers=headers
26
+ )
27
+
28
+ self.status_code: HTTPStatus = status_code
29
+ self.error_code: str = error_code
30
+ self.message: str = message
31
+ self.display_message: str = display_message
32
+
33
+ def __str__(self):
34
+ return (
35
+ f"[{self.status_code}] {self.error_code}: {self.message} "
36
+ f"(Display: {self.display_message})"
37
+ )
38
+
39
+ def to_dict(self) -> dict[str, Any]:
40
+ return {
41
+ "status_code": self.status_code.value,
42
+ "error_code": self.error_code,
43
+ "message": self.message,
44
+ "display_message": self.display_message,
45
+ }
@@ -0,0 +1,64 @@
1
+ from uuid import UUID, uuid4
2
+
3
+ from typing import Optional
4
+
5
+ from sqlmodel import SQLModel, Field
6
+ from datetime import datetime, timezone
7
+
8
+
9
+ class BaseModel(SQLModel):
10
+ id: UUID = Field(default_factory=uuid4, primary_key=True)
11
+ created_at: Optional[datetime] = Field(
12
+ default_factory=lambda: datetime.now(timezone.utc)
13
+ )
14
+ updated_at: Optional[datetime] = Field(
15
+ default_factory=lambda: datetime.now(timezone.utc),
16
+ sa_column_kwargs = {"onupdate": lambda: datetime.now(timezone.utc)}
17
+ )
18
+
19
+
20
+ # App specific models
21
+ ## Integrations
22
+ class IntegrationsModel(BaseModel):
23
+ pass
24
+
25
+
26
+ class IntegrationsOrganizationModel(IntegrationsModel):
27
+ organization: UUID = Field(index=True)
28
+
29
+
30
+ ## Vacancies
31
+ class VacanciesModel(BaseModel):
32
+ pass
33
+
34
+
35
+ class VacanciesOrganizationModel(VacanciesModel):
36
+ organization: UUID = Field(index=True)
37
+
38
+
39
+ ## Campaigns
40
+ class CampaignsModel(BaseModel):
41
+ pass
42
+
43
+
44
+ class CampaignsOrganizationModel(CampaignsModel):
45
+ organization: UUID = Field(index=True)
46
+
47
+
48
+
49
+ ## Billing
50
+ class BillingModel(BaseModel):
51
+ pass
52
+
53
+
54
+ class BillingOrganizationModel(BillingModel):
55
+ organization: UUID = Field(index=True)
56
+
57
+
58
+ ## IAM
59
+ class IAMModel(BaseModel):
60
+ pass
61
+
62
+
63
+ class IAMOrganizationModel(BaseModel):
64
+ organization: UUID = Field(index=True)
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ from uuid import UUID
2
+
3
+ from sqlmodel import Field
4
+
5
+ from ..general.models import IAMModel, IAMOrganizationModel
6
+ from sqlalchemy import Column, JSON
7
+
8
+
9
+ class CompositeRole(IAMModel, table=True):
10
+ name: str = Field(index=True)
11
+ roles: list = Field(sa_column=Column(JSON))
12
+ organization: UUID = Field(index=True, nullable=True)
13
+ manageable_roles: list = Field(sa_column=Column(JSON))
14
+ permission_level: int = Field(default=0)
15
+
16
+
17
+ class MemberRoleConnection(IAMOrganizationModel, table=True):
18
+ composite_role_id: UUID = Field(foreign_key="compositerole.id")
19
+ member: UUID = Field(index=True, nullable=False)
@@ -0,0 +1,12 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+ @dataclass
5
+ class Organization:
6
+ id: str
7
+ name: str
8
+ displayName: str
9
+ attributes: dict
10
+ roles: List[str]
11
+ url: str
12
+ role: str
@@ -0,0 +1,34 @@
1
+ from uuid import UUID
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class Integration(BaseModel):
9
+ id: UUID
10
+ created_at: datetime
11
+ updated_at: Optional[datetime]
12
+ organization: UUID
13
+
14
+ name: str
15
+ icon: str
16
+ type: str
17
+ tag: str
18
+ enabled: bool
19
+ description: str
20
+ code_reference: str
21
+ setup_config: dict
22
+ order: int
23
+
24
+
25
+ class Link(BaseModel):
26
+ id: UUID
27
+ created_at: datetime
28
+ updated_at: Optional[datetime]
29
+ organization: UUID
30
+
31
+ name: str
32
+ status: str
33
+ auth_config: dict
34
+ integration_id: UUID
@@ -0,0 +1,25 @@
1
+ from uuid import UUID
2
+
3
+ from sqlalchemy import Column, JSON
4
+ from sqlmodel import Field
5
+
6
+ from ..general.models import IntegrationsOrganizationModel, IntegrationsModel
7
+
8
+
9
+ class Integration(IntegrationsModel, table=True):
10
+ name: str = Field(index=True)
11
+ icon: str = Field(index=True)
12
+ type: str = Field(index=True)
13
+ tag: str = Field(index=True, nullable=True)
14
+ enabled: bool = Field(default=True, nullable=True)
15
+ description: str = Field(index=True, nullable=True)
16
+ code_reference: str = Field(index=True)
17
+ setup_config: dict = Field(sa_column=Column(JSON))
18
+ order: int = Field(default=0)
19
+
20
+
21
+ class Link(IntegrationsOrganizationModel, table=True):
22
+ name: str = Field(index=True)
23
+ status: str = Field(index=True)
24
+ auth_config: dict = Field(sa_column=Column(JSON))
25
+ integration_id: UUID = Field(foreign_key="integration.id")
File without changes
@@ -0,0 +1,6 @@
1
+ def to_enum(enum_cls, value):
2
+ if value is None:
3
+ return None
4
+ if isinstance(value, enum_cls):
5
+ return value
6
+ return enum_cls(value)
@@ -0,0 +1,172 @@
1
+ from datetime import datetime
2
+ from typing import List, Optional
3
+ from uuid import UUID
4
+ from dataclasses import field
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from ..util.enum import to_enum
9
+ from ..vacancies.models import Feed, Vacancy as VacancyModel, SalaryFrequency, RemoteType
10
+
11
+
12
+ class VacancyLocation(BaseModel):
13
+ zip_code: str | None = None
14
+ city: str | None = None
15
+ address: str | None = None
16
+ state: str | None = None
17
+ country: str | None = None
18
+
19
+
20
+ class Salary(BaseModel):
21
+ min: float | None = None
22
+ max: float | None = None
23
+ currency: str = "EUR"
24
+ frequency: SalaryFrequency = "month"
25
+
26
+
27
+ class Hours(BaseModel):
28
+ min: int = 0
29
+ max: int = 40
30
+ fte: float = 1.0
31
+
32
+
33
+ class ContactDetails(BaseModel):
34
+ email: str | None = None
35
+ first_name: str | None = None
36
+ last_name: str | None = None
37
+ phone_number: str | None = None
38
+ role: str | None = None
39
+
40
+
41
+ class VacancyData(BaseModel):
42
+
43
+ # Required fields
44
+ reference_number: str
45
+ requisition_id: str
46
+ title: str
47
+ description: str
48
+ job_site_url: str
49
+ company_name: str
50
+ publish_date: datetime | None = None
51
+ category: List[str] = field(default_factory=list)
52
+ experience: List[str] = field(default_factory=list)
53
+ education: List[str] = field(default_factory=list)
54
+
55
+ # Connected data
56
+ hours: Hours = field(default_factory=Hours)
57
+ location: VacancyLocation = field(default_factory=VacancyLocation)
58
+ salary: Salary = field(default_factory=Salary)
59
+ recruiter: ContactDetails = field(default_factory=ContactDetails)
60
+
61
+ # Optional fields
62
+ status: str | None = None
63
+ parent_company_name: str | None = None
64
+ remote_type: RemoteType | None = None
65
+ expiration_date: datetime | None = None
66
+ last_updated_date: datetime | None = None
67
+
68
+ def to_model(self, feed: Feed):
69
+ return VacancyModel(
70
+ feed_id=feed.id,
71
+ organization=feed.organization,
72
+ reference_number=self.reference_number,
73
+ requisition_id=self.requisition_id,
74
+ title=self.title,
75
+ description=self.description,
76
+ status=self.status,
77
+ job_site_url=self.job_site_url,
78
+ company_name=self.company_name,
79
+ parent_company_name=self.parent_company_name,
80
+ remote_type=self.remote_type.value if self.remote_type else None,
81
+ publish_date=self.publish_date,
82
+ expiration_date=self.expiration_date,
83
+ last_updated_date=self.last_updated_date,
84
+ category=self.category,
85
+ experience=self.experience,
86
+ education=self.education,
87
+ hours_fte=self.hours.fte,
88
+ hours_min=self.hours.min,
89
+ hours_max=self.hours.max,
90
+ location_address=self.location.address,
91
+ location_zipcode=self.location.zip_code,
92
+ location_city=self.location.city,
93
+ location_state=self.location.state,
94
+ location_country=self.location.country,
95
+ salary_min=self.salary.min,
96
+ salary_max=self.salary.max,
97
+ salary_currency=self.salary.currency,
98
+ salary_frequency=self.salary.frequency.value if self.salary.frequency else SalaryFrequency.MONTH,
99
+ recruiter_first_name=self.recruiter.first_name,
100
+ recruiter_last_name=self.recruiter.last_name,
101
+ recruiter_phone_number=self.recruiter.phone_number,
102
+ recruiter_email=self.recruiter.email,
103
+ recruiter_role=self.recruiter.role,
104
+ )
105
+
106
+ class Vacancy(VacancyData):
107
+ id: UUID
108
+ created_at: datetime
109
+ updated_at: Optional[datetime]
110
+ organization: UUID
111
+
112
+ @classmethod
113
+ def from_model(cls: "Vacancy", model: VacancyModel) -> "Vacancy":
114
+ hours = Hours(
115
+ min=model.hours_min,
116
+ max=model.hours_max,
117
+ fte=model.hours_fte,
118
+ )
119
+
120
+ location = VacancyLocation(
121
+ address=getattr(model, "location_address", None),
122
+ zip_code=getattr(model, "location_zipcode", None),
123
+ city=getattr(model, "location_city", None),
124
+ state=getattr(model, "location_state", None),
125
+ country=getattr(model, "location_country", None),
126
+ )
127
+
128
+ salary = Salary(
129
+ min=getattr(model, "salary_min", None),
130
+ max=getattr(model, "salary_max", None),
131
+ currency=getattr(model, "salary_currency", "EUR") or "EUR",
132
+ frequency=to_enum(SalaryFrequency, getattr(model, "salary_frequency", SalaryFrequency.MONTH) or SalaryFrequency.MONTH),
133
+ )
134
+
135
+ recruiter = ContactDetails(
136
+ first_name=getattr(model, "recruiter_first_name", None),
137
+ last_name=getattr(model, "recruiter_last_name", None),
138
+ phone_number=getattr(model, "recruiter_phone_number", None),
139
+ email=getattr(model, "recruiter_email", None),
140
+ role=getattr(model, "recruiter_role", None),
141
+ )
142
+
143
+ remote_type = to_enum(RemoteType, getattr(model, "remote_type", None))
144
+
145
+ return cls(
146
+ organization=getattr(model, "organization", None),
147
+ id=model.id,
148
+ created_at=model.created_at,
149
+ updated_at=model.updated_at,
150
+
151
+ reference_number=model.reference_number,
152
+ requisition_id=model.requisition_id,
153
+ title=model.title,
154
+ description=model.description,
155
+ job_site_url=model.job_site_url,
156
+ company_name=model.company_name,
157
+ publish_date=model.publish_date,
158
+ category=list(model.category or []),
159
+ experience=list(model.experience or []),
160
+ education=list(model.education or []),
161
+
162
+ hours=hours,
163
+ location=location,
164
+ salary=salary,
165
+ recruiter=recruiter,
166
+
167
+ status=getattr(model, "status", None),
168
+ parent_company_name=getattr(model, "parent_company_name", None),
169
+ remote_type=remote_type,
170
+ expiration_date=getattr(model, "expiration_date", None),
171
+ last_updated_date=getattr(model, "last_updated_date", None),
172
+ )
@@ -0,0 +1,81 @@
1
+ from enum import StrEnum
2
+ from uuid import UUID
3
+
4
+ from datetime import datetime
5
+ from typing import Optional, List
6
+
7
+ from sqlalchemy import Column, JSON, UniqueConstraint, Enum
8
+ from sqlmodel import Field
9
+
10
+ from ..general.models import VacanciesOrganizationModel
11
+
12
+
13
+ class SalaryFrequency(StrEnum):
14
+ MONTH = "month"
15
+ YEAR = "year"
16
+ WEEK = "week"
17
+ HOUR = "hour"
18
+
19
+ class RemoteType(StrEnum):
20
+ ONSITE = "onsite"
21
+ HYBRID = "hybrid"
22
+ REMOTE = "remote"
23
+
24
+
25
+ class Feed(VacanciesOrganizationModel, table=True):
26
+ name: str = Field(index=True)
27
+ status: str = Field(index=True)
28
+ file_url: Optional[str] = Field()
29
+ ats_link_id: Optional[UUID] = Field()
30
+ last_sync_date: Optional[datetime] = Field()
31
+ synced_vacancy_count: int = Field(default=0)
32
+ mapping: dict = Field(sa_column=Column(JSON))
33
+ custom_fields: dict = Field(sa_column=Column(JSON))
34
+
35
+
36
+ class Vacancy(VacanciesOrganizationModel, table=True):
37
+ feed_id: UUID = Field(foreign_key="feed.id")
38
+ reference_number: str = Field(index=True)
39
+ requisition_id: str = Field(index=True)
40
+ title: str = Field(index=True)
41
+ description: str = Field()
42
+ status: str = Field()
43
+ job_site_url: str = Field()
44
+ company_name: str = Field(index=True)
45
+ parent_company_name: Optional[str] = Field()
46
+ remote_type: Optional[RemoteType] = Field(sa_column=Column(Enum(RemoteType)))
47
+ publish_date: Optional[datetime] = Field()
48
+ expiration_date: Optional[datetime] = Field()
49
+ last_updated_date: Optional[datetime] = Field()
50
+ category: List[str] = Field(sa_column=Column(JSON))
51
+ experience: List[str] = Field(sa_column=Column(JSON))
52
+ education: List[str] = Field(sa_column=Column(JSON))
53
+ hours_fte: Optional[float] = Field(default=1.0)
54
+ hours_min: Optional[int] = Field(default=1)
55
+ hours_max: Optional[int] = Field(default=40)
56
+ location_address: Optional[str] = Field()
57
+ location_zipcode: Optional[str] = Field()
58
+ location_city: Optional[str] = Field()
59
+ location_state: Optional[str] = Field()
60
+ location_country: Optional[str] = Field()
61
+ salary_min: Optional[float] = Field()
62
+ salary_max: Optional[float] = Field()
63
+ salary_currency: str = Field(default="EUR")
64
+ salary_frequency: SalaryFrequency = Field(sa_column=Column(Enum(SalaryFrequency)))
65
+ recruiter_first_name: Optional[str] = Field()
66
+ recruiter_last_name: Optional[str] = Field()
67
+ recruiter_phone_number: Optional[str] = Field()
68
+ recruiter_email: Optional[str] = Field()
69
+ recruiter_role: Optional[str] = Field()
70
+
71
+ __table_args__ = (
72
+ UniqueConstraint("feed_id", "reference_number", name="uq_feed_reference"),
73
+ )
74
+
75
+
76
+ class Modification(VacanciesOrganizationModel, table=True):
77
+ name: str = Field(index=True)
78
+ type: str = Field(index=True)
79
+ selector_configuration: dict = Field(sa_column=Column(JSON))
80
+ changes_configuration: dict = Field(sa_column=Column(JSON))
81
+