brynq-sdk-alight 1.0.0.dev0__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.
Files changed (44) hide show
  1. brynq_sdk_alight/__init__.py +1058 -0
  2. brynq_sdk_alight/address.py +72 -0
  3. brynq_sdk_alight/archive/flat_wrapper.py +139 -0
  4. brynq_sdk_alight/archive/hrxml_generator.py +280 -0
  5. brynq_sdk_alight/archive/managers.py +132 -0
  6. brynq_sdk_alight/archive/managers_generic.py +114 -0
  7. brynq_sdk_alight/archive/managers_old_complex.py +294 -0
  8. brynq_sdk_alight/archive/managers_simple.py +229 -0
  9. brynq_sdk_alight/employee.py +81 -0
  10. brynq_sdk_alight/job.py +89 -0
  11. brynq_sdk_alight/leave.py +97 -0
  12. brynq_sdk_alight/pay_elements.py +97 -0
  13. brynq_sdk_alight/salary.py +89 -0
  14. brynq_sdk_alight/schemas/__init__.py +26 -0
  15. brynq_sdk_alight/schemas/absence.py +83 -0
  16. brynq_sdk_alight/schemas/address.py +113 -0
  17. brynq_sdk_alight/schemas/employee.py +656 -0
  18. brynq_sdk_alight/schemas/generated_envelope_xsd_schema/__init__.py +38683 -0
  19. brynq_sdk_alight/schemas/generated_envelope_xsd_schema/process_pay_serv_emp.py +622264 -0
  20. brynq_sdk_alight/schemas/generated_xsd_schemas/__init__.py +10965 -0
  21. brynq_sdk_alight/schemas/generated_xsd_schemas/csec_person.py +39808 -0
  22. brynq_sdk_alight/schemas/generated_xsd_schemas/hrxml_indicative_data.py +90318 -0
  23. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_bod.py +33869 -0
  24. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_currency_code_iso_7_04.py +365 -0
  25. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_language_code_iso_7_04.py +16 -0
  26. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_mimemedia_type_code_iana_7_04.py +16 -0
  27. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_unit_code_unece_7_04.py +14 -0
  28. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_lists.py +535 -0
  29. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_qualified_data_types.py +84 -0
  30. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_unqualified_data_types.py +1449 -0
  31. brynq_sdk_alight/schemas/job.py +145 -0
  32. brynq_sdk_alight/schemas/leave.py +58 -0
  33. brynq_sdk_alight/schemas/payments.py +207 -0
  34. brynq_sdk_alight/schemas/salary.py +67 -0
  35. brynq_sdk_alight/schemas/termination.py +48 -0
  36. brynq_sdk_alight/schemas/timequota.py +66 -0
  37. brynq_sdk_alight/schemas/utils.py +452 -0
  38. brynq_sdk_alight/termination.py +103 -0
  39. brynq_sdk_alight/time_elements.py +121 -0
  40. brynq_sdk_alight/time_quotas.py +114 -0
  41. brynq_sdk_alight-1.0.0.dev0.dist-info/METADATA +20 -0
  42. brynq_sdk_alight-1.0.0.dev0.dist-info/RECORD +44 -0
  43. brynq_sdk_alight-1.0.0.dev0.dist-info/WHEEL +5 -0
  44. brynq_sdk_alight-1.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,81 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+
4
+ class Employee:
5
+ """
6
+ High-level Employee API (no manager layer).
7
+
8
+ Usage:
9
+ alight = Alight()
10
+ xml = alight.employee.create({...})
11
+ xml = alight.employee.update(EmployeeModel(...))
12
+ """
13
+
14
+ def __init__(self, client: Any):
15
+ self._client = client
16
+
17
+ def create(
18
+ self,
19
+ data: Dict[str, Any],
20
+ *,
21
+ logical_id: Optional[str] = None,
22
+ extension_data: Optional[Dict[str, Any]] = None,
23
+ pay_elements_data: Optional[Dict[str, Any]] = None,
24
+ envelope_options: Optional[Dict[str, Any]] = None,
25
+ pretty_print: bool = True,
26
+ ) -> str:
27
+ """
28
+ Create a New Hire XML document (actionCode=ADD).
29
+ Returns XML string.
30
+
31
+ Example:
32
+ >>> alight.employee.create(
33
+ ... {
34
+ ... "person_id": "35561",
35
+ ... "employee_id": "35561ZZGB",
36
+ ... "given_name": "Alex",
37
+ ... "family_name": "Mason",
38
+ ... },
39
+ ... extension_data={"bank_accounts": [{"iban": "GB00BARC20201530093459"}]},
40
+ ... )
41
+ """
42
+ return self._client.generate_employee_xml(
43
+ employee=data,
44
+ action_code="ADD",
45
+ logical_id=logical_id,
46
+ extension_data=extension_data,
47
+ pay_elements_data=pay_elements_data,
48
+ envelope_options=envelope_options,
49
+ pretty_print=pretty_print,
50
+ )
51
+
52
+ def update(
53
+ self,
54
+ data: Dict[str, Any],
55
+ *,
56
+ logical_id: Optional[str] = None,
57
+ extension_data: Optional[Dict[str, Any]] = None,
58
+ pay_elements_data: Optional[Dict[str, Any]] = None,
59
+ envelope_options: Optional[Dict[str, Any]] = None,
60
+ pretty_print: bool = True,
61
+ ) -> str:
62
+ """
63
+ Create an Employee Change XML document (actionCode=CHANGE).
64
+ Returns XML string.
65
+
66
+ Example:
67
+ >>> alight.employee.update(
68
+ ... {"person_id": "35561", "employee_id": "35561ZZGB", "email": "alex@example.com"},
69
+ ... extension_data={"bank_accounts": [{"iban": "GB00BARC20201530093459"}]},
70
+ ... pay_elements_data={"pay_element": [{"id": [{"value": "0010"}], "amount": {"value": "45000"}}]},
71
+ ... )
72
+ """
73
+ return self._client.generate_employee_xml(
74
+ employee=data,
75
+ action_code="CHANGE",
76
+ logical_id=logical_id,
77
+ extension_data=extension_data,
78
+ pay_elements_data=pay_elements_data,
79
+ envelope_options=envelope_options,
80
+ pretty_print=pretty_print,
81
+ )
@@ -0,0 +1,89 @@
1
+ from typing import Any, Dict, Optional, Mapping
2
+
3
+ from .schemas import Job as JobModel
4
+
5
+
6
+ class Job:
7
+ """
8
+ High-level Job/Deployment API.
9
+
10
+ Usage:
11
+ alight = Alight()
12
+ xml = alight.job.create(data={...}, person_id="35561", employee_id="35561ZZGB")
13
+ xml = alight.job.update(data={...}, person_id="35561", employee_id="35561ZZGB")
14
+ """
15
+
16
+ def __init__(self, client: Any):
17
+ self._client = client
18
+
19
+ def create(
20
+ self,
21
+ data: Dict[str, Any],
22
+ *,
23
+ person_id: str,
24
+ employee_id: str,
25
+ logical_id: Optional[str] = None,
26
+ envelope_options: Optional[Dict[str, Any]] = None,
27
+ pretty_print: bool = True,
28
+ ) -> str:
29
+ """
30
+ Create job/deployment data (actionCode=ADD). Returns XML string.
31
+ Requires both person_id and employee_id.
32
+
33
+ Example:
34
+ >>> alight.job.create(
35
+ ... data={"job_code": "FIN-MGR", "department": "Finance"},
36
+ ... person_id="35561",
37
+ ... employee_id="35561ZZGB",
38
+ ... )
39
+ """
40
+ job_model = JobModel(**data)
41
+ payload = {
42
+ "person_id": person_id,
43
+ "employee_id": employee_id,
44
+ "job": job_model.model_dump(exclude_none=True, by_alias=True),
45
+ }
46
+
47
+ return self._client.generate_employee_xml(
48
+ employee=payload,
49
+ action_code="ADD",
50
+ logical_id=logical_id,
51
+ envelope_options=envelope_options,
52
+ pretty_print=pretty_print,
53
+ )
54
+
55
+ def update(
56
+ self,
57
+ data: Mapping[str, Any],
58
+ *,
59
+ person_id: str,
60
+ employee_id: str,
61
+ logical_id: Optional[str] = None,
62
+ envelope_options: Optional[dict[str, Any]] = None,
63
+ pretty_print: bool = True,
64
+ ) -> str:
65
+ """
66
+ Update job/deployment data (actionCode=CHANGE). Returns XML string.
67
+ Requires both person_id and employee_id.
68
+
69
+ Example:
70
+ >>> alight.job.update(
71
+ ... data={"job": {"job_code": "FIN-DIR", "effective_date": "2024-04-01"}},
72
+ ... person_id="35561",
73
+ ... employee_id="35561ZZGB",
74
+ ... )
75
+ """
76
+ job_model = JobModel(**dict(data))
77
+ payload = {
78
+ "person_id": person_id,
79
+ "employee_id": employee_id,
80
+ "job": job_model.model_dump(exclude_none=True, by_alias=True),
81
+ }
82
+
83
+ return self._client.generate_employee_xml(
84
+ employee=payload,
85
+ action_code="CHANGE",
86
+ logical_id=logical_id,
87
+ envelope_options=envelope_options,
88
+ pretty_print=pretty_print,
89
+ )
@@ -0,0 +1,97 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ from .schemas import Leave as LeaveModel
4
+
5
+
6
+ class Leave:
7
+ """
8
+ High-level Leave API.
9
+
10
+ Usage:
11
+ alight = Alight()
12
+ xml = alight.leave.create(data={...}, person_id="35561", employee_id="35561ZZGB")
13
+ xml = alight.leave.update(data={...}, person_id="35561", employee_id="35561ZZGB")
14
+ """
15
+
16
+ def __init__(self, client: Any):
17
+ self._client = client
18
+
19
+ def create(
20
+ self,
21
+ data: Dict[str, Any],
22
+ *,
23
+ person_id: str,
24
+ employee_id: str,
25
+ logical_id: Optional[str] = None,
26
+ envelope_options: Optional[Dict[str, Any]] = None,
27
+ pretty_print: bool = True,
28
+ ) -> str:
29
+ """
30
+ Create leave entry (actionCode=ADD). Returns XML string.
31
+ Requires both person_id and employee_id.
32
+
33
+ Example:
34
+ >>> alight.leave.create(
35
+ ... data={
36
+ ... "leave_type": "PARENTAL",
37
+ ... "start_date": "2024-02-01",
38
+ ... "end_date": "2024-02-15",
39
+ ... },
40
+ ... person_id="35561",
41
+ ... employee_id="35561ZZGB",
42
+ ... )
43
+ """
44
+ leave_model = LeaveModel(**data)
45
+ payload = {
46
+ "person_id": person_id,
47
+ "employee_id": employee_id,
48
+ "leave": leave_model.model_dump(exclude_none=True, by_alias=True),
49
+ }
50
+
51
+ return self._client.generate_employee_xml(
52
+ employee=payload,
53
+ action_code="ADD",
54
+ logical_id=logical_id,
55
+ envelope_options=envelope_options,
56
+ pretty_print=pretty_print,
57
+ )
58
+
59
+ def update(
60
+ self,
61
+ data: Dict[str, Any],
62
+ *,
63
+ person_id: str,
64
+ employee_id: str,
65
+ logical_id: Optional[str] = None,
66
+ envelope_options: Optional[dict[str, Any]] = None,
67
+ pretty_print: bool = True,
68
+ ) -> str:
69
+ """
70
+ Update leave entry (actionCode=CHANGE). Returns XML string.
71
+ Requires both person_id and employee_id.
72
+
73
+ Example:
74
+ >>> alight.leave.update(
75
+ ... data={"leave": {"leave_type": "SICK", "status": "Cancelled"}},
76
+ ... person_id="35561",
77
+ ... employee_id="35561ZZGB",
78
+ ... )
79
+ """
80
+ if 'leave' in data and data['leave'] is not None:
81
+ nested = data['leave']
82
+ leave_model = nested if isinstance(nested, LeaveModel) else LeaveModel(**nested)
83
+ else:
84
+ leave_model = LeaveModel(**data)
85
+ payload = {
86
+ "person_id": person_id,
87
+ "employee_id": employee_id,
88
+ "leave": leave_model.model_dump(exclude_none=True, by_alias=True),
89
+ }
90
+
91
+ return self._client.generate_employee_xml(
92
+ employee=payload,
93
+ action_code="CHANGE",
94
+ logical_id=logical_id,
95
+ envelope_options=envelope_options,
96
+ pretty_print=pretty_print,
97
+ )
@@ -0,0 +1,97 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from .schemas.payments import PayElementCreate
4
+ from .schemas.generated_envelope_xsd_schema.process_pay_serv_emp import (
5
+ PayServEmpPayElements as XsdPayServEmpPayElements,
6
+ )
7
+
8
+
9
+ class PayElements:
10
+ """
11
+ High-level PayElements API. Builds PayServEmpPayElements from a list of PayElementCreate.
12
+
13
+ Usage:
14
+ alight = Alight()
15
+ xml = alight.pay_elements.create(elements=[{...}], person_id="35561", employee_id="35561ZZGB")
16
+ xml = alight.pay_elements.update(elements=[{...}], person_id="35561", employee_id="35561ZZGB")
17
+ """
18
+
19
+ def __init__(self, client: Any):
20
+ self._client = client
21
+
22
+ def _build_pay_elements(self, elements: List[Dict[str, Any]]) -> Dict[str, Any]:
23
+ """
24
+ Normalize incoming pay elements and validate them against the generated XSD model.
25
+
26
+ Accepts plain dictionaries and returns the nested payload expected by `generate_employee_xml`.
27
+
28
+ Example:
29
+ >>> self._build_pay_elements([
30
+ ... {"element_code": "0010", "amount": 1200, "currency_code": "GBP"},
31
+ ... ])
32
+ """
33
+ normalized: List[Dict[str, Any]] = []
34
+ for item in elements:
35
+ model = item if isinstance(item, PayElementCreate) else PayElementCreate(**item)
36
+ normalized.append(model.to_nested_dict())
37
+ payload = {"pay_element": normalized}
38
+ XsdPayServEmpPayElements.model_validate(payload)
39
+ return payload
40
+
41
+ def create(
42
+ self,
43
+ *,
44
+ elements: List[Dict[str, Any]],
45
+ person_id: str,
46
+ employee_id: str,
47
+ logical_id: Optional[str] = None,
48
+ envelope_options: Optional[Dict[str, Any]] = None,
49
+ pretty_print: bool = True,
50
+ ) -> str:
51
+ """
52
+ Generate an ADD envelope for pay elements using the minimal person identifiers.
53
+
54
+ Example:
55
+ >>> alight.pay_elements.create(
56
+ ... elements=[{"element_code": "0010", "amount": "1200", "currency_code": "GBP"}],
57
+ ... person_id="35561",
58
+ ... employee_id="35561ZZGB",
59
+ ... )
60
+ """
61
+ pay_elements_data = self._build_pay_elements(elements)
62
+
63
+ return self._client.generate_employee_xml(
64
+ employee={"person_id": person_id, "employee_id": employee_id},
65
+ action_code="ADD",
66
+ logical_id=logical_id,
67
+ pay_elements_data=pay_elements_data,
68
+ envelope_options=envelope_options,
69
+ pretty_print=pretty_print,
70
+ )
71
+
72
+ def update(
73
+ self,
74
+ *,
75
+ elements: List[Dict[str, Any]],
76
+ person_id: str,
77
+ employee_id: str,
78
+ logical_id: Optional[str] = None,
79
+ envelope_options: Optional[Dict[str, Any]] = None,
80
+ pretty_print: bool = True,
81
+ ) -> str:
82
+ """
83
+ Generate a CHANGE envelope for existing pay elements while keeping identifiers flat.
84
+
85
+ Mirrors `create` but reuses the CHANGE action code so downstream consumers can differ updates
86
+ from new allocations without rebuilding envelopes manually.
87
+ """
88
+ pay_elements_data = self._build_pay_elements(elements)
89
+
90
+ return self._client.generate_employee_xml(
91
+ employee={"person_id": person_id, "employee_id": employee_id},
92
+ action_code="CHANGE",
93
+ logical_id=logical_id,
94
+ pay_elements_data=pay_elements_data,
95
+ envelope_options=envelope_options,
96
+ pretty_print=pretty_print,
97
+ )
@@ -0,0 +1,89 @@
1
+ from typing import Any, Dict, Optional, Union
2
+ from xsdata.models.datatype import XmlDate
3
+
4
+ from .schemas import Salary as SalaryModel, EmployeeCreate as EmployeeModel
5
+
6
+
7
+ class Salary:
8
+ """
9
+ High-level Salary API (no manager layer).
10
+
11
+ Usage:
12
+ alight = Alight()
13
+ xml = alight.salary.update(salary={...}, identifiers={"person_id": "35561", "employee_id": "35561ZZGB"})
14
+ """
15
+
16
+ def __init__(self, client: Any):
17
+ self._client = client
18
+
19
+ def update(
20
+ self,
21
+ salary: Union[SalaryModel, Dict[str, Any]],
22
+ *,
23
+ identifiers: Optional[Dict[str, Any]] = None,
24
+ employee: Optional[Union[EmployeeModel, Dict[str, Any]]] = None,
25
+ logical_id: Optional[str] = None,
26
+ pretty_print: bool = True,
27
+ ) -> str:
28
+ """
29
+ Create an Employee Change XML with salary change as PayElements.
30
+ Returns XML string.
31
+
32
+ Example:
33
+ >>> alight.salary.update(
34
+ ... salary={"base_salary": 42000, "currency_code": "GBP"},
35
+ ... identifiers={"person_id": "35561", "employee_id": "35561ZZGB"},
36
+ ... )
37
+
38
+ Passing `salary` as a plain dict is enough—the helper wraps it in the generated `Salary` schema
39
+ and derives the recurring pay element payload automatically.
40
+ """
41
+ # Build or augment an EmployeeModel with identifiers and salary
42
+ if employee is None:
43
+ if not identifiers or "person_id" not in identifiers:
44
+ raise ValueError("identifiers must include at least 'person_id' when no employee is provided")
45
+ base: Dict[str, Any] = {
46
+ "person_id": identifiers.get("person_id"),
47
+ }
48
+ if identifiers.get("employee_id"):
49
+ base["employee_id"] = identifiers.get("employee_id")
50
+ employee_model = EmployeeModel(**base)
51
+ else:
52
+ employee_model = employee if isinstance(employee, EmployeeModel) else EmployeeModel(**employee)
53
+
54
+ # Normalize salary model
55
+ salary_model = salary if isinstance(salary, SalaryModel) else SalaryModel(**salary)
56
+
57
+ # Convert salary to alias-based nested dict compatible with to_model mapping
58
+ # Alight expects a default pay element code/type even if upstream sources omit them,
59
+ # so fall back to the recurring base salary identifiers that match standard setup.
60
+ pay_elements_data = {
61
+ "pay_element": [
62
+ {
63
+ "id": [{"value": salary_model.element_code or "0010"}],
64
+ "pay_element_type": {"value": salary_model.element_type or "RECURRING"},
65
+ "amount": {"value": f"{salary_model.base_salary:.2f}"},
66
+ "currency_code": {"value": salary_model.currency_code},
67
+ "valid_from": (
68
+ XmlDate.from_string(salary_model.valid_from.isoformat())
69
+ if salary_model.valid_from
70
+ else None
71
+ ),
72
+ }
73
+ ]
74
+ }
75
+
76
+ # Clean None values in pay element
77
+ for k in list(pay_elements_data["pay_element"][0].keys()):
78
+ if pay_elements_data["pay_element"][0][k] is None:
79
+ del pay_elements_data["pay_element"][0][k]
80
+
81
+ # Also mirror salary on the Employee flat fields so IndicativeData contains identifiers
82
+ # Employee.to_model handles wrapping; pay elements go via pay_elements_data
83
+ return self._client.generate_employee_xml(
84
+ employee=employee_model,
85
+ action_code="CHANGE",
86
+ logical_id=logical_id,
87
+ pay_elements_data=pay_elements_data,
88
+ pretty_print=pretty_print,
89
+ )
@@ -0,0 +1,26 @@
1
+ """
2
+ Alight SDK schema models.
3
+ """
4
+
5
+ from .employee import EmployeeCreate
6
+ from .salary import Salary
7
+ from .address import Address
8
+ from .job import Job
9
+ from .leave import Leave
10
+ from .termination import Termination
11
+ from .absence import Absence
12
+ from .timequota import TimeQuota
13
+ from .payments import PayServEmpExtensionCreate, PayElementCreate
14
+
15
+ __all__ = [
16
+ "EmployeeCreate",
17
+ "Salary",
18
+ "Address",
19
+ "Job",
20
+ "Leave",
21
+ "Termination",
22
+ "Absence",
23
+ "TimeQuota",
24
+ "PayServEmpExtensionCreate",
25
+ "PayElementCreate",
26
+ ]
@@ -0,0 +1,83 @@
1
+ """
2
+ Flat, user-friendly absence (time element) model for Alight SDK.
3
+ """
4
+
5
+ import datetime
6
+ from typing import Optional, Dict, Any, List
7
+ from pydantic import Field, BaseModel
8
+
9
+ from .utils import add_to_nested_path, convert_datetime_to_xml, post_process_nested_data
10
+
11
+
12
+ class Absence(BaseModel):
13
+ """
14
+ Simplified time element mapping to PayServEmpTimeElements.time_element[0].
15
+ """
16
+ model_config = {
17
+ "populate_by_name": True
18
+ }
19
+
20
+ # Core identifiers and reason
21
+ id: Optional[str] = Field(default=None, alias="time_element[0].id[0]")
22
+ absence_reason: Optional[str] = Field(default=None, alias="time_element[0].absence_reason")
23
+
24
+ # Quantities
25
+ units: Optional[str] = Field(default=None, alias="time_element[0].units")
26
+ unit_type: Optional[str] = Field(default=None, alias="time_element[0].unit_type")
27
+
28
+ # Dates
29
+ valid_from: Optional[datetime.date] = Field(default=None, alias="time_element[0].valid_from")
30
+ valid_to: Optional[datetime.date] = Field(default=None, alias="time_element[0].valid_to")
31
+
32
+ # Easy additional fields
33
+ comments: Optional[str] = Field(default=None, alias="time_element[0].comments")
34
+ absence_post: Optional[str] = Field(default=None, alias="time_element[0].absence_post")
35
+
36
+ def to_nested_dict(self) -> Dict[str, Any]:
37
+ flat = self.model_dump(exclude_none=True, by_alias=True)
38
+ nested: Dict[str, Any] = {}
39
+ for k, v in flat.items():
40
+ add_to_nested_path(nested, k, v)
41
+ # Convert dates to XML compatible
42
+ for key in ("valid_from", "valid_to"):
43
+ if key in nested and isinstance(nested[key], (datetime.date, datetime.datetime)):
44
+ nested[key] = convert_datetime_to_xml(nested[key], None)
45
+ return nested
46
+
47
+
48
+ class Absences(BaseModel):
49
+ """
50
+ Wrapper for multiple Absence items mapping to time_element[*].
51
+ """
52
+ absences: Optional[List[Absence]] = Field(default=None, description="List of absence time elements")
53
+
54
+ def to_nested_dict(self) -> Dict[str, Any]:
55
+ nested: Dict[str, Any] = {}
56
+ if not self.absences:
57
+ return nested
58
+ # Merge each absence into time_element list
59
+ time_elements: List[Dict[str, Any]] = []
60
+ for idx, item in enumerate(self.absences):
61
+ data = item.to_nested_dict()
62
+ # Convert indexed paths from [0] to [idx]
63
+ remapped: Dict[str, Any] = {}
64
+ for k, v in data.items():
65
+ remapped[k.replace("time_element[0]", f"time_element[{idx}]")] = v
66
+ # Build nested for this item and then merge
67
+ item_nested: Dict[str, Any] = {}
68
+ for k, v in remapped.items():
69
+ add_to_nested_path(item_nested, k, v)
70
+ # Collect the single time_element dict
71
+ if "time_element" in item_nested and isinstance(item_nested["time_element"], list):
72
+ time_elements.extend(item_nested["time_element"])
73
+ if time_elements:
74
+ nested["time_element"] = time_elements
75
+ # Post-process using the XSD model to wrap units and convert dates
76
+ try:
77
+ from ..schemas.generated_envelope_xsd_schema.process_pay_serv_emp import (
78
+ PayServEmpTimeElements as XsdPayServEmpTimeElements,
79
+ )
80
+ post_process_nested_data(nested, XsdPayServEmpTimeElements)
81
+ except Exception:
82
+ pass
83
+ return nested
@@ -0,0 +1,113 @@
1
+ """
2
+ Flat, user-friendly address model for Alight SDK.
3
+ """
4
+
5
+ from typing import Optional, Dict, Any, List
6
+ from pydantic import Field, BaseModel
7
+
8
+ # Composition approach: plain Pydantic BaseModel; conversion handled by callers
9
+
10
+
11
+ class Address(BaseModel):
12
+ """
13
+ Simplified address model.
14
+ Uses PURE schema-driven conversion - NO hardcoded structure mappings.
15
+ Uses aliases to match expected field names in Employee model.
16
+ """
17
+ model_config = {
18
+ "populate_by_name": True # Allow populating by field name in addition to alias
19
+ }
20
+
21
+ line_1: str = Field(
22
+ description="Street address line 1",
23
+ alias="indicative_person_dossier.indicative_person.communication[2].address.line_one"
24
+ )
25
+ line_2: Optional[str] = Field(
26
+ default=None,
27
+ description="Street address line 2",
28
+ alias="indicative_person_dossier.indicative_person.communication[2].address.line_two"
29
+ )
30
+ line_3: Optional[str] = Field(
31
+ default=None,
32
+ description="Street address line 3",
33
+ alias="indicative_person_dossier.indicative_person.communication[2].address.line_three"
34
+ )
35
+ line_4: Optional[str] = Field(
36
+ default=None,
37
+ description="Street address line 4",
38
+ alias="indicative_person_dossier.indicative_person.communication[2].address.line_four"
39
+ )
40
+ line_5: Optional[str] = Field(
41
+ default=None,
42
+ description="Street address line 5",
43
+ alias="indicative_person_dossier.indicative_person.communication[2].address.line_five"
44
+ )
45
+ building_number: Optional[str] = Field(
46
+ default=None,
47
+ description="Building number",
48
+ alias="indicative_person_dossier.indicative_person.communication[2].address.building_number"
49
+ )
50
+ building_name: Optional[str] = Field(
51
+ default=None,
52
+ description="Building name",
53
+ alias="indicative_person_dossier.indicative_person.communication[2].address.building_name"
54
+ )
55
+ street_name: Optional[str] = Field(
56
+ default=None,
57
+ description="Street name",
58
+ alias="indicative_person_dossier.indicative_person.communication[2].address.street_name"
59
+ )
60
+ unit: Optional[str] = Field(
61
+ default=None,
62
+ description="Unit or apartment identifier",
63
+ alias="indicative_person_dossier.indicative_person.communication[2].address.unit"
64
+ )
65
+ floor: Optional[str] = Field(
66
+ default=None,
67
+ description="Floor number",
68
+ alias="indicative_person_dossier.indicative_person.communication[2].address.floor"
69
+ )
70
+ post_office_box: Optional[str] = Field(
71
+ default=None,
72
+ description="P.O. Box",
73
+ alias="indicative_person_dossier.indicative_person.communication[2].address.post_office_box"
74
+ )
75
+ city: str = Field(
76
+ description="City name",
77
+ alias="indicative_person_dossier.indicative_person.communication[2].address.city_name"
78
+ )
79
+ state_province: Optional[str] = Field(
80
+ default=None,
81
+ description="State/province",
82
+ alias="indicative_person_dossier.indicative_person.communication[2].address.country_sub_division_code"
83
+ )
84
+ delivery_point_code: Optional[List[str]] = Field(
85
+ default=None,
86
+ description="Delivery point code(s)",
87
+ alias="indicative_person_dossier.indicative_person.communication[2].address.delivery_point_code"
88
+ )
89
+ postal_code: str = Field(
90
+ description="ZIP/postal code",
91
+ alias="indicative_person_dossier.indicative_person.communication[2].address.postal_code"
92
+ )
93
+ country: str = Field(
94
+ description="Country code (GB/US/etc.)",
95
+ alias="indicative_person_dossier.indicative_person.communication[2].address.country_code"
96
+ )
97
+ city_sub_division_name: Optional[List[str]] = Field(
98
+ default=None,
99
+ description="Neighborhood or city subdivision name(s)",
100
+ alias="indicative_person_dossier.indicative_person.communication[2].address.city_sub_division_name"
101
+ )
102
+ use_code: Optional[str] = Field(
103
+ default=None,
104
+ description="Address use context (HOME/WORK)",
105
+ alias="indicative_person_dossier.indicative_person.communication[2].use_code"
106
+ )
107
+
108
+ def to_nested_dict(self) -> Dict[str, Any]:
109
+ """
110
+ Convert to nested structure compatible with employee address fields.
111
+ """
112
+ # Convert to dictionary with aliases - will directly map to employee fields
113
+ return self.model_dump(exclude_none=True, by_alias=True)