-
Notifications
You must be signed in to change notification settings - Fork 3
fix(spp_dci_client_dr): parse DCI v1.0.0 spec envelope and record fields #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+969
−227
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| from . import dr_parsing | ||
| from . import dr_service |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| # Part of OpenSPP. See LICENSE file for full copyright and licensing details. | ||
| """Pure parsing helpers for DCI v1.0.0 Disability Registry records. | ||
|
|
||
| All functions are stateless and require no Odoo env, making them | ||
| independently testable and easy to reuse. | ||
| """ | ||
|
|
||
| import logging | ||
| from datetime import date, datetime | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
| # Status values that indicate the person is registered as disabled. | ||
| # The SP DCI v1.0.0 spec does not declare an enum; these are the | ||
| # spec-aligned workflow tokens. Empty / missing / other values trigger | ||
| # the impairment-list fallback in extract_disability_data. | ||
| _APPROVED_STATUSES = {"approved", "registered"} | ||
|
|
||
| # Status values that explicitly reject disability registration. | ||
| _REJECTED_STATUSES = {"rejected", "denied"} | ||
|
|
||
|
|
||
| def _coerce_date(value) -> date | None: | ||
| """Coerce a DCI date/datetime value into a ``date`` object. | ||
|
|
||
| Accepts ISO date strings (``YYYY-MM-DD``), ISO datetime strings with an | ||
| optional trailing ``Z`` (``YYYY-MM-DDTHH:MM:SSZ``), naive/aware datetimes, | ||
| and date objects. Returns ``None`` for empty input or unparseable values | ||
| (with a WARNING logged). | ||
|
|
||
| For tz-aware inputs, the local wall-clock date is returned; | ||
| no UTC normalization is applied. | ||
| """ | ||
| if not value: | ||
| return None | ||
| if isinstance(value, datetime): | ||
| return value.date() | ||
| if isinstance(value, date): | ||
| return value | ||
| try: | ||
| return datetime.fromisoformat(str(value).removesuffix("Z")).date() | ||
| except ValueError: | ||
| _logger.warning("Could not parse date from value: %r", value) | ||
| return None | ||
|
|
||
|
|
||
| def unwrap_search_data(data) -> list: | ||
| """Extract the reg_records list from a DCI v1.0.0 search response data envelope. | ||
|
|
||
| Args: | ||
| data: The value of ``search_response[*].data`` from the API response. | ||
| Expected to be a dict with a ``reg_records`` key per the spec. | ||
|
|
||
| Returns: | ||
| list: The contents of ``data["reg_records"]``, or an empty list when | ||
| the envelope is absent, empty, or malformed. | ||
| """ | ||
| if data is None: | ||
| return [] | ||
|
|
||
| if not isinstance(data, dict): | ||
| _logger.warning( | ||
| "Unexpected type for search response data envelope: %s; expected dict", | ||
| type(data).__name__, | ||
| ) | ||
| return [] | ||
|
|
||
| if not data: | ||
| return [] | ||
|
|
||
| records = data.get("reg_records") | ||
| if records is None: | ||
| return [] | ||
| if not isinstance(records, list): | ||
| _logger.warning( | ||
| "Unexpected type for reg_records: %s; expected list", | ||
| type(records).__name__, | ||
| ) | ||
| return [] | ||
| return records | ||
|
|
||
|
|
||
| def extract_disability_data(record: dict) -> dict: | ||
| """Extract structured disability information from a DCI v1.0.0 record. | ||
|
|
||
| Args: | ||
| record: A single record dict from ``reg_records``. | ||
|
|
||
| Returns: | ||
| dict with keys: | ||
| - ``has_disability`` (bool) | ||
| - ``disability_types`` (list[str]) | ||
| - ``functional_scores`` (dict, always ``{}``: spec has no numeric scores) | ||
| - ``assessment_date`` (``date`` | None) | ||
| - ``source_registry`` (str | None) | ||
| - ``raw_data`` (the input record, unchanged) | ||
| """ | ||
| # Extract impairment types from disability_details. | ||
| # Use `or []` so an explicit null on the wire does not crash the loop. | ||
| details = record.get("disability_details") or [] | ||
| disability_types = [d["impairment_type"] for d in details if isinstance(d, dict) and d.get("impairment_type")] | ||
|
|
||
| # Resolve has_disability from the disability_status string | ||
| status_str = str(record.get("disability_status", "")).strip().lower() | ||
|
|
||
| if status_str in _APPROVED_STATUSES: | ||
| has_disability = True | ||
| elif status_str in _REJECTED_STATUSES: | ||
| has_disability = False | ||
| elif status_str == "": | ||
| # No explicit status: fall back to impairment list presence | ||
| has_disability = bool(disability_types) | ||
| else: | ||
| _logger.warning( | ||
| "Unknown disability_status value: %s; falling back to impairment list", | ||
| record.get("disability_status"), | ||
| ) | ||
| has_disability = bool(disability_types) | ||
|
|
||
| # Assessment date: prefer last_updated, fall back to registration_date. | ||
| # The spec uses ISO datetime strings; coerce to a date for the ORM. | ||
| assessment_date = _coerce_date(record.get("last_updated") or record.get("registration_date")) | ||
|
|
||
| # Source registry: prefer source_registry, fall back to registry_name | ||
| source_registry = record.get("source_registry") or record.get("registry_name") | ||
|
|
||
| return { | ||
| "has_disability": has_disability, | ||
| "disability_types": disability_types, | ||
| "functional_scores": {}, | ||
| "assessment_date": assessment_date, | ||
| "source_registry": source_registry, | ||
| "raw_data": record, | ||
| } | ||
|
|
||
|
|
||
| def extract_functional_scores(record: dict) -> dict: | ||
| """Return functional assessment scores from a DCI v1.0.0 record. | ||
|
|
||
| The DCI v1.0.0 spec does not define numeric functional scores. | ||
| ``impairment_level`` is a free-text string, not a number. | ||
| This function always returns ``{}`` and exists as a hook for future | ||
| spec versions that may introduce numeric scoring. | ||
|
|
||
| Args: | ||
| record: A single record dict from ``reg_records``. | ||
|
|
||
| Returns: | ||
| dict: Always ``{}``. | ||
| """ | ||
| return {} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| # Part of OpenSPP. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from . import test_callback | ||
| from . import test_disability_status | ||
| from . import test_dr_parsing | ||
| from . import test_dr_service |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the
disability_statusfield is explicitlynullin the JSON payload,record.get("disability_status", "")returnsNone. Passing this tostr()results in the string"none"(after.lower()), which is not one of the expected status tokens. This causes the parser to fall through to theelseblock at line 113, logging an "Unknown disability_status value: None" warning. Usingor ""inside thestr()call ensures thatnullvalues are treated as empty strings, which are correctly handled as an ambiguous status without triggering a warning.