brynq-sdk-alight 1.0.0.dev0__py3-none-any.whl → 1.0.0.dev2__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.
@@ -413,7 +413,6 @@ class Alight(BrynQ):
413
413
  # BOD REMOVAL: Always include creation_date_time and bodid for model validation (required fields)
414
414
  # We'll remove them from XML output later if exclude_bod_fields is True
415
415
  app_area_data["creation_date_time"] = {"value": XmlDateTime.from_datetime(timestamp)}
416
- # BOD REMOVAL: Only include bodid if not excluding BOD fields
417
416
  if not exclude_bod_fields:
418
417
  app_area_data["bodid"] = {"value": app_bodid_value}
419
418
  if application_language_code:
@@ -537,17 +536,21 @@ class Alight(BrynQ):
537
536
  pydantic_serializer = PydanticXmlSerializer()
538
537
  raw_xml = pydantic_serializer.render(envelope, ns_map=namespace_map)
539
538
 
540
- # BOD REMOVAL: Remove BOD fields from XML if exclude_bod_fields was set (for new hires)
539
+ # BOD REMOVAL: Remove BOD fields from XML if exclude_bod_fields was set (for new hires and updates)
541
540
  # Check if we should exclude by looking at the envelope's application_area
542
541
  # When exclude_bod_fields is True, bodid is not set, so it will be None
543
- if envelope.application_area and envelope.application_area.bodid is None:
544
- # Remove CreationDateTime and BODID elements from XML for new hires
545
- # Handle both single-line and multi-line XML formatting
546
- raw_xml = re.sub(r'<oa:CreationDateTime>.*?</oa:CreationDateTime>\s*', '', raw_xml, flags=re.DOTALL)
547
- raw_xml = re.sub(r'<oa:BODID>.*?</oa:BODID>\s*', '', raw_xml, flags=re.DOTALL)
548
- # Also handle self-closing tags if they exist
549
- raw_xml = re.sub(r'<oa:CreationDateTime\s*/>\s*', '', raw_xml)
550
- raw_xml = re.sub(r'<oa:BODID\s*/>\s*', '', raw_xml)
542
+ # We always remove creation_date_time and bodid from XML when exclude_bod_fields is True
543
+ if envelope.application_area:
544
+ bodid = getattr(envelope.application_area, 'bodid', None)
545
+ # If bodid is None, it means exclude_bod_fields was True, so remove both fields
546
+ if bodid is None:
547
+ # Remove CreationDateTime and BODID elements from XML
548
+ # Handle both single-line and multi-line XML formatting
549
+ raw_xml = re.sub(r'<oa:CreationDateTime>.*?</oa:CreationDateTime>\s*', '', raw_xml, flags=re.DOTALL)
550
+ raw_xml = re.sub(r'<oa:BODID>.*?</oa:BODID>\s*', '', raw_xml, flags=re.DOTALL)
551
+ # Also handle self-closing tags if they exist
552
+ raw_xml = re.sub(r'<oa:CreationDateTime\s*/>\s*', '', raw_xml)
553
+ raw_xml = re.sub(r'<oa:BODID\s*/>\s*', '', raw_xml)
551
554
 
552
555
  if pretty_print:
553
556
  # Pretty print the XML
@@ -807,11 +810,11 @@ class Alight(BrynQ):
807
810
  auth_token = self.session.headers.get('Authorization', 'Bearer <TOKEN>')
808
811
  subscription_key = self.session.headers.get('Ocp-Apim-Subscription-Key', '<SUBSCRIPTION_KEY>')
809
812
 
810
- print(f'curl {self.base_url}/bods/submit -X POST -H "Content-Type: application/json" -H "gcc: {self.gcc}" -H "env: {env}" -H "Authorization: {auth_token}" -H "Ocp-Apim-Subscription-Key: {subscription_key}" -d \'{{"bod": "{data}"}}\'')
813
+ # print(f'curl {self.base_url}/bods/submit -X POST -H "Content-Type: application/json" -H "gcc: {self.gcc}" -H "env: {env}" -H "Authorization: {auth_token}" -H "Ocp-Apim-Subscription-Key: {subscription_key}" -d \'{{"bod": "{data}"}}\'')
811
814
 
812
815
  resp = self.session.post(url=f"{self.base_url}/bods/submit", json={"bod": data})
813
816
  resp.raise_for_status()
814
- return resp.json().get("UUID")
817
+ return resp.json().get("bodId")
815
818
 
816
819
  def get_bods_status(self, bods_id: str):
817
820
  """
@@ -863,7 +866,16 @@ class Alight(BrynQ):
863
866
 
864
867
  try:
865
868
  # Convert Employee to IndicativeDataType model
866
- employee_model = employee if isinstance(employee, EmployeeCreate) else EmployeeCreate(**employee)
869
+ if isinstance(employee, EmployeeCreate):
870
+ employee_model = employee
871
+ # Set action_code if not already set
872
+ if not hasattr(employee_model, 'action_code') or employee_model.action_code is None:
873
+ employee_model.action_code = action_code
874
+ else:
875
+ # Pass action_code to model for CHANGE vs ADD detection
876
+ employee_dict = dict(employee)
877
+ employee_dict['action_code'] = action_code
878
+ employee_model = EmployeeCreate(**employee_dict)
867
879
  indicative_data = employee_model.to_model()
868
880
 
869
881
  # Process optional extensions
@@ -956,8 +968,8 @@ class Alight(BrynQ):
956
968
  envelope_kwargs.setdefault("bod_id", str(uuid.uuid4()).upper())
957
969
  envelope_kwargs.setdefault("logical_id", logical_id or "TST-GB003-1001")
958
970
 
959
- # BOD REMOVAL: Exclude BOD fields for new hires (action_code="ADD")
960
- if action_code == "ADD":
971
+ # BOD REMOVAL: Exclude BOD fields for new hires (action_code="ADD") and updates (action_code="CHANGE")
972
+ if action_code == "ADD" or action_code == "CHANGE":
961
973
  envelope_kwargs["exclude_bod_fields"] = True
962
974
 
963
975
  envelope = self.create_complete_alight_envelope(
@@ -24,7 +24,7 @@ class Address:
24
24
  data: Dict[str, Any],
25
25
  *,
26
26
  person_id: str,
27
- employee_id: str,
27
+ employee_id: Optional[str] = None,
28
28
  logical_id: Optional[str] = None,
29
29
  envelope_options: Optional[Dict[str, Any]] = None,
30
30
  employee_data: Optional[Dict[str, Any]] = None,
@@ -32,7 +32,7 @@ class Address:
32
32
  ) -> str:
33
33
  """
34
34
  Update address (actionCode=CHANGE). Returns XML string.
35
- Requires both person_id and employee_id per integration constraints.
35
+ Requires person_id. employee_id is optional for updates.
36
36
 
37
37
  Example:
38
38
  >>> alight.address.update(
@@ -43,13 +43,13 @@ class Address:
43
43
  ... "address_valid_from": "2024-01-01",
44
44
  ... },
45
45
  ... person_id="35561",
46
- ... employee_id="35561ZZGB",
47
46
  ... )
48
47
  """
49
48
  payload: dict[str, Any] = {
50
49
  "person_id": person_id,
51
- "employee_id": employee_id,
52
50
  }
51
+ if employee_id is not None:
52
+ payload["employee_id"] = employee_id
53
53
 
54
54
  # build from flat fields
55
55
  address_model = AddressModel(**data)
brynq_sdk_alight/job.py CHANGED
@@ -57,28 +57,28 @@ class Job:
57
57
  data: Mapping[str, Any],
58
58
  *,
59
59
  person_id: str,
60
- employee_id: str,
60
+ employee_id: Optional[str] = None,
61
61
  logical_id: Optional[str] = None,
62
62
  envelope_options: Optional[dict[str, Any]] = None,
63
63
  pretty_print: bool = True,
64
64
  ) -> str:
65
65
  """
66
66
  Update job/deployment data (actionCode=CHANGE). Returns XML string.
67
- Requires both person_id and employee_id.
67
+ Requires person_id. employee_id is optional for updates.
68
68
 
69
69
  Example:
70
70
  >>> alight.job.update(
71
71
  ... data={"job": {"job_code": "FIN-DIR", "effective_date": "2024-04-01"}},
72
72
  ... person_id="35561",
73
- ... employee_id="35561ZZGB",
74
73
  ... )
75
74
  """
76
75
  job_model = JobModel(**dict(data))
77
76
  payload = {
78
77
  "person_id": person_id,
79
- "employee_id": employee_id,
80
78
  "job": job_model.model_dump(exclude_none=True, by_alias=True),
81
79
  }
80
+ if employee_id is not None:
81
+ payload["employee_id"] = employee_id
82
82
 
83
83
  return self._client.generate_employee_xml(
84
84
  employee=payload,
brynq_sdk_alight/leave.py CHANGED
@@ -61,20 +61,19 @@ class Leave:
61
61
  data: Dict[str, Any],
62
62
  *,
63
63
  person_id: str,
64
- employee_id: str,
64
+ employee_id: Optional[str] = None,
65
65
  logical_id: Optional[str] = None,
66
66
  envelope_options: Optional[dict[str, Any]] = None,
67
67
  pretty_print: bool = True,
68
68
  ) -> str:
69
69
  """
70
70
  Update leave entry (actionCode=CHANGE). Returns XML string.
71
- Requires both person_id and employee_id.
71
+ Requires person_id. employee_id is optional for updates.
72
72
 
73
73
  Example:
74
74
  >>> alight.leave.update(
75
75
  ... data={"leave": {"leave_type": "SICK", "status": "Cancelled"}},
76
76
  ... person_id="35561",
77
- ... employee_id="35561ZZGB",
78
77
  ... )
79
78
  """
80
79
  if 'leave' in data and data['leave'] is not None:
@@ -84,9 +83,10 @@ class Leave:
84
83
  leave_model = LeaveModel(**data)
85
84
  payload = {
86
85
  "person_id": person_id,
87
- "employee_id": employee_id,
88
86
  "leave": leave_model.model_dump(exclude_none=True, by_alias=True),
89
87
  }
88
+ if employee_id is not None:
89
+ payload["employee_id"] = employee_id
90
90
 
91
91
  return self._client.generate_employee_xml(
92
92
  employee=payload,
@@ -74,7 +74,7 @@ class PayElements:
74
74
  *,
75
75
  elements: List[Dict[str, Any]],
76
76
  person_id: str,
77
- employee_id: str,
77
+ employee_id: Optional[str] = None,
78
78
  logical_id: Optional[str] = None,
79
79
  envelope_options: Optional[Dict[str, Any]] = None,
80
80
  pretty_print: bool = True,
@@ -87,8 +87,12 @@ class PayElements:
87
87
  """
88
88
  pay_elements_data = self._build_pay_elements(elements)
89
89
 
90
+ employee_payload = {"person_id": person_id}
91
+ if employee_id is not None:
92
+ employee_payload["employee_id"] = employee_id
93
+
90
94
  return self._client.generate_employee_xml(
91
- employee={"person_id": person_id, "employee_id": employee_id},
95
+ employee=employee_payload,
92
96
  action_code="CHANGE",
93
97
  logical_id=logical_id,
94
98
  pay_elements_data=pay_elements_data,
@@ -4,16 +4,17 @@ Automatically converts to complex nested HR-XML structure using schema introspec
4
4
  """
5
5
 
6
6
  import datetime
7
- from typing import Optional, Dict, Any, List, Union
8
- from pydantic import Field, model_validator, BaseModel
7
+ from typing import Any, Dict, List, Optional, Union
8
+
9
+ from pydantic import BaseModel, Field, model_validator
9
10
 
10
- from .utils import add_to_nested_path, post_process_nested_data, construct_model
11
- from .salary import Salary # Import related models
12
11
  from .address import Address
12
+ from .generated_xsd_schemas.hrxml_indicative_data import IndicativeDataType
13
13
  from .job import Job
14
14
  from .leave import Leave
15
+ from .salary import Salary # Import related models
15
16
  from .termination import Termination
16
- from .generated_xsd_schemas.hrxml_indicative_data import IndicativeDataType
17
+ from .utils import add_to_nested_path, construct_model, post_process_nested_data
17
18
 
18
19
 
19
20
  class EmployeeCreate(BaseModel):
@@ -478,6 +479,9 @@ class EmployeeCreate(BaseModel):
478
479
  leave: Optional[Leave] = Field(default=None, description="Employee's leave information")
479
480
  termination: Optional[Termination] = Field(default=None, description="Employee's termination information")
480
481
 
482
+ # Internal field to track action code (excluded from model dump)
483
+ action_code: Optional[str] = Field(default=None, exclude=True, description="Internal: action code (ADD/CHANGE)")
484
+
481
485
  # Schedule Information and Work Level fields removed - these belong in Job model
482
486
  # Access these through the job relation
483
487
 
@@ -584,8 +588,10 @@ class EmployeeCreate(BaseModel):
584
588
  raise ImportError("IndicativeDataType not available - cannot perform schema-driven conversion")
585
589
 
586
590
  # Dump current model using aliases to get dot-path keys
591
+ # exclude_unset=True prevents default values (like work_level_code="FullTime") from being included
592
+ # unless they were explicitly set, which is important for CHANGE updates
587
593
  flat_alias: Dict[str, Any] = self.model_dump(
588
- exclude={"salary", "address", "job"}, exclude_none=True, by_alias=True
594
+ exclude={"salary", "address", "job", "action_code"}, exclude_none=True, exclude_unset=True, by_alias=True
589
595
  )
590
596
  # print("[EMP] flat_alias keys:", list(flat_alias.keys())[:40])
591
597
 
@@ -605,9 +611,13 @@ class EmployeeCreate(BaseModel):
605
611
  del flat_alias[basis_path]
606
612
 
607
613
  # Minimal business logic
608
- if not flat_alias.get('indicative_person_dossier.indicative_employee.employee_id') and self.person_id:
609
- # Mirror employee_id default to person_id if missing
610
- flat_alias['indicative_person_dossier.indicative_employee.employee_id'] = self.person_id
614
+ # Only auto-add employee_id for ADD operations, not for CHANGE
615
+ employee_id_key = 'indicative_person_dossier.indicative_employee.employee_id'
616
+ if employee_id_key not in flat_alias and self.person_id:
617
+ # Only auto-add employee_id for ADD operations
618
+ # For CHANGE operations, employee_id should be explicitly provided if needed
619
+ if self.action_code != "CHANGE":
620
+ flat_alias[employee_id_key] = self.person_id
611
621
 
612
622
  # Merge related models (they already dump with by_alias in their to_nested_dict)
613
623
  if self.salary is not None:
@@ -64,7 +64,7 @@ class Termination:
64
64
  data: Dict[str, Any],
65
65
  *,
66
66
  person_id: str,
67
- employee_id: str,
67
+ employee_id: Optional[str] = None,
68
68
  extension_data: Optional[Dict[str, Any]] = None,
69
69
  employee_data: Optional[Dict[str, Any]] = None,
70
70
  logical_id: Optional[str] = None,
@@ -73,22 +73,22 @@ class Termination:
73
73
  ) -> str:
74
74
  """
75
75
  Update termination entry (actionCode=CHANGE). Returns XML string.
76
- Requires both person_id and employee_id.
76
+ Requires person_id. employee_id is optional for updates.
77
77
 
78
78
  Example:
79
79
  >>> alight.termination.update(
80
80
  ... data={"termination_reason": "DISMISSAL", "termination_date": "2024-07-15"},
81
81
  ... person_id="35561",
82
- ... employee_id="35561ZZGB",
83
82
  ... employee_data={"last_day_worked": "2024-07-10"},
84
83
  ... )
85
84
  """
86
85
  termination_model = TerminationModel(**data)
87
86
  payload: Dict[str, Any] = {
88
87
  "person_id": person_id,
89
- "employee_id": employee_id,
90
88
  "termination": termination_model.model_dump(exclude_none=True, by_alias=True),
91
89
  }
90
+ if employee_id is not None:
91
+ payload["employee_id"] = employee_id
92
92
 
93
93
  if employee_data:
94
94
  payload.update(employee_data)
@@ -84,7 +84,7 @@ class TimeElements:
84
84
  *,
85
85
  absences: Union[Dict[str, Any], List[Dict[str, Any]]],
86
86
  person_id: str,
87
- employee_id: str,
87
+ employee_id: Optional[str] = None,
88
88
  logical_id: Optional[str] = None,
89
89
  envelope_options: Optional[Dict[str, Any]] = None,
90
90
  pretty_print: bool = True,
@@ -101,15 +101,15 @@ class TimeElements:
101
101
  ... {"absence_reason": "VAC", "valid_from": "2024-01-01", "valid_to": "2024-01-02", "units": "16"}
102
102
  ... ],
103
103
  ... person_id="35561",
104
- ... employee_id="35561ZZGB",
105
104
  ... )
106
105
  """
107
106
  extension_data = self._build_extension_with_time_elements(absences)
108
107
 
109
108
  payload = {
110
109
  "person_id": person_id,
111
- "employee_id": employee_id,
112
110
  }
111
+ if employee_id is not None:
112
+ payload["employee_id"] = employee_id
113
113
 
114
114
  return self._client.generate_employee_xml(
115
115
  employee=payload,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brynq_sdk_alight
3
- Version: 1.0.0.dev0
3
+ Version: 1.0.0.dev2
4
4
  Summary: Alight SDK for the BrynQ.com platform
5
5
  Author: BrynQ
6
6
  Author-email: support@brynq.com
@@ -1,12 +1,12 @@
1
- brynq_sdk_alight/__init__.py,sha256=Xtxcewz98rTAQww2NdMIZh9zUwvdwsFmyLgcxOKCiow,45941
2
- brynq_sdk_alight/address.py,sha256=xi14zO_l2hdXMVzxDcKaeEyhh6y0LBhgidTZ6ws0dek,2180
1
+ brynq_sdk_alight/__init__.py,sha256=31a_PT0Ep5bCR8Rr7_NHfmTQogGRtANPXC7__mwNnR4,46680
2
+ brynq_sdk_alight/address.py,sha256=kB-9JFvRPXDEpYuDkudJK_UcaR-eolb6n8sVt6XLr_0,2185
3
3
  brynq_sdk_alight/employee.py,sha256=9E-zlgD7eSPiS7p5brRPpf9X9JFSsyETkv5bXfqv0dU,2692
4
- brynq_sdk_alight/job.py,sha256=DqWqBXcqMKQbDsIPu9zcE79hzE0jHewPjVBb2eI-6ic,2683
5
- brynq_sdk_alight/leave.py,sha256=7hYWbkxXFLWEjzBqallYCOg6kVGXxd_FCsRsE_TXTns,2993
6
- brynq_sdk_alight/pay_elements.py,sha256=XJA1KexjHX8ottQWSsgD8jip2M5LmLlhiTU3Ldp51co,3519
4
+ brynq_sdk_alight/job.py,sha256=FAM0gB3ZKocMYN7EQmoct7XLmtL76HdDMFcexr3qCIM,2716
5
+ brynq_sdk_alight/leave.py,sha256=PXoMw3OEZ7b7KU2ztRC74CLGFdk8EMohJYLW32oX_vA,3026
6
+ brynq_sdk_alight/pay_elements.py,sha256=H4LKS__AbT3qogueYdlPKl1jF_4soEyz8qUtj-5Il64,3647
7
7
  brynq_sdk_alight/salary.py,sha256=KfSYW-WrW4L0OqxfE2oUTDJERaCmhCTptRSft07NsrA,3716
8
- brynq_sdk_alight/termination.py,sha256=IhvJ61GteSnOx2v98eDf2WOIhLn1jDdIdio5N4kCNQI,3427
9
- brynq_sdk_alight/time_elements.py,sha256=ssYHzpx_hJ-f7RTyYpq_7OrfZBdPUrxfU5Fx418TabM,4360
8
+ brynq_sdk_alight/termination.py,sha256=6TcBXBBedF4xmWsf8h_zsMV8Yc50gSAmC-m_3xmjaCA,3460
9
+ brynq_sdk_alight/time_elements.py,sha256=9ZjmglACi6YT0UNkyKTA-1B-CxFz8QIydBBOv8I1NUk,4377
10
10
  brynq_sdk_alight/time_quotas.py,sha256=dag3OEzUge81IX61093SZ_RwJGynBY0zcuVn0XO7xb0,4143
11
11
  brynq_sdk_alight/archive/flat_wrapper.py,sha256=-5yK5P3daAItmSIXfqfiPssSZ3Lje-dItGBqNAhwSB8,5677
12
12
  brynq_sdk_alight/archive/hrxml_generator.py,sha256=b52bfGmLKUsCPGFtMr6KWCcc11XPAf8GOR4v7UBAVOU,9852
@@ -17,7 +17,7 @@ brynq_sdk_alight/archive/managers_simple.py,sha256=EiHTVocRAmBQdO4ipxXAQtgdIPrM5
17
17
  brynq_sdk_alight/schemas/__init__.py,sha256=nDqu5nKvrts-cDHNWYbBJWz-ouq6BtOazdwT1z165nc,537
18
18
  brynq_sdk_alight/schemas/absence.py,sha256=2BlTvF3ecRfYqF27QvkKo5J6k1ZMFypSZpuhY6pPuWo,3402
19
19
  brynq_sdk_alight/schemas/address.py,sha256=Rl6dqE8piDQv1oA8PA7IId14uVp03vK21DGopP4dPiM,4473
20
- brynq_sdk_alight/schemas/employee.py,sha256=880woXJTPBxxM4gai6CVvmjgM2RQ2NKUCqBTcDNLTcw,27925
20
+ brynq_sdk_alight/schemas/employee.py,sha256=FHqtpIWJLSYqgEomzIu9_wJN-iUE0fdgL-l4tPiljuY,28535
21
21
  brynq_sdk_alight/schemas/job.py,sha256=DF2u4IhEg2kWLlzL7Qel02FFZD8KTD43P5YfUA-thCM,6073
22
22
  brynq_sdk_alight/schemas/leave.py,sha256=BUJ7V5I4c-t-U6Keyaoqr81FfguzoxtJ4-Yg6e1e0q8,2183
23
23
  brynq_sdk_alight/schemas/payments.py,sha256=69O40UoOa8FG7VAWSWk-x_c7co2Sxn_LG95hYkCYpOI,15331
@@ -38,7 +38,7 @@ brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_unit_c
38
38
  brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_lists.py,sha256=YSjUKQI3j0BxDIk3jOipJ8bSvFRH3M7OU4g7lFqP4Gc,15244
39
39
  brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_qualified_data_types.py,sha256=fwhRh2bkBdMvpRpRnITAv-1wiYKf1H20HCeIWa0ziyE,2696
40
40
  brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_unqualified_data_types.py,sha256=cbea-P7N45ktP7tFcQMZvKbvTE9-x81NV7CumP4G10I,69646
41
- brynq_sdk_alight-1.0.0.dev0.dist-info/METADATA,sha256=1yD4MSkfHX4R_nqxSiPZ0gcOpSYeP_ikKhk2VjuMGG4,502
42
- brynq_sdk_alight-1.0.0.dev0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
- brynq_sdk_alight-1.0.0.dev0.dist-info/top_level.txt,sha256=4S5DPwjBXkLhf4SonK9dvZsdc5ijVT8Zeyro8vv82-I,17
44
- brynq_sdk_alight-1.0.0.dev0.dist-info/RECORD,,
41
+ brynq_sdk_alight-1.0.0.dev2.dist-info/METADATA,sha256=pABVEFAmoOF1x-sub7dfMaPGhPkB91eKcLT60yQB6As,502
42
+ brynq_sdk_alight-1.0.0.dev2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
+ brynq_sdk_alight-1.0.0.dev2.dist-info/top_level.txt,sha256=4S5DPwjBXkLhf4SonK9dvZsdc5ijVT8Zeyro8vv82-I,17
44
+ brynq_sdk_alight-1.0.0.dev2.dist-info/RECORD,,