acis-core 1.0.0__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.
acis_core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Reference data and utilities shared by ACIS clients."""
acis_core/a_numbers.py ADDED
@@ -0,0 +1,26 @@
1
+ """Utilities for working with A-Numbers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from acis_core.exceptions import AcisError
8
+
9
+
10
+ class InvalidANumberError(AcisError):
11
+ """Raised when an A-Number isn't valid."""
12
+
13
+
14
+ _NON_DIGITS = re.compile(r"[\s\-]")
15
+
16
+
17
+ def normalize_a_number(value: str) -> str:
18
+ candidate = _NON_DIGITS.sub("", value or "")
19
+ candidate = candidate.removeprefix("A").removeprefix("a")
20
+ if not candidate.isdigit() or len(candidate) != 9:
21
+ raise InvalidANumberError(f"A-Number must be 9 digits, got {value!r}")
22
+ return candidate
23
+
24
+
25
+ def redact(a_number: str) -> str:
26
+ return f"***{a_number[-4:]}" if len(a_number) >= 4 else "***"
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class AcisError(Exception):
7
+ """Base class for failures talking to ACIS."""
8
+
9
+ def __init__(self, *args: object, payload: dict[str, Any] | None = None) -> None:
10
+ super().__init__(*args)
11
+ self.payload = payload
@@ -0,0 +1,65 @@
1
+ """Utilities for working with EOIR nationality codes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from functools import cache
8
+ from pathlib import Path
9
+
10
+ from acis_core.exceptions import AcisError
11
+
12
+ HERE = Path(__file__).parent
13
+ REFERENCE_FILE = HERE / "reference" / "nationality-codes.json"
14
+
15
+
16
+ class UnknownNationalityError(AcisError):
17
+ """Raised when a nationality code or name can't be resolved."""
18
+
19
+
20
+ # Placeholder entry in the upstream table; never a valid lookup value.
21
+ UNKNOWN_CODE = "??"
22
+
23
+
24
+ @dataclass(frozen=True, kw_only=True)
25
+ class Nationality:
26
+ code: str
27
+ name: str
28
+
29
+
30
+ @cache
31
+ def _tables() -> tuple[dict[str, Nationality], dict[str, Nationality]]:
32
+ """Return (by_code, by_name) lookup tables of active nationalities."""
33
+ raw = json.loads(REFERENCE_FILE.read_text(encoding="utf-8"))
34
+ by_code: dict[str, Nationality] = {}
35
+ by_name: dict[str, Nationality] = {}
36
+ for entry in raw:
37
+ code = entry["Code"].strip().upper()
38
+ if not entry.get("IsActive") or code == UNKNOWN_CODE:
39
+ continue
40
+ nationality = Nationality(code=code, name=entry["Name"].strip().upper())
41
+ by_code[nationality.code] = nationality
42
+ by_name[nationality.name] = nationality
43
+ return by_code, by_name
44
+
45
+
46
+ def get_by_code(code: str) -> Nationality:
47
+ """Look up strictly by code."""
48
+ by_code, _ = _tables()
49
+ try:
50
+ return by_code[code.strip().upper()]
51
+ except KeyError:
52
+ raise UnknownNationalityError(f"Unknown nationality code: {code!r}") from None
53
+
54
+
55
+ def resolve(value: str) -> Nationality:
56
+ """Resolve a code (``MX``) or a name (``mexico``) to a Nationality."""
57
+ candidate = (value or "").strip().upper()
58
+ if not candidate:
59
+ raise UnknownNationalityError("Nationality is required")
60
+ by_code, by_name = _tables()
61
+ if candidate in by_code:
62
+ return by_code[candidate]
63
+ if candidate in by_name:
64
+ return by_name[candidate]
65
+ raise UnknownNationalityError(f"Unknown nationality: {value!r}")
acis_core/outcome.py ADDED
@@ -0,0 +1,38 @@
1
+ """Classification of ACIS response payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum, auto
6
+ from typing import Any
7
+
8
+
9
+ class Outcome(StrEnum):
10
+ """Classification of an ACIS payload by its pinned ``message`` fragments."""
11
+
12
+ OK = auto()
13
+ CAPTCHA_REJECTED = auto()
14
+ UNAVAILABLE = auto()
15
+ NOT_FOUND = auto()
16
+ INVALID_NATIONALITY = auto()
17
+ OTHER = auto()
18
+
19
+ @classmethod
20
+ def from_payload(cls, payload: dict[str, Any]) -> Outcome:
21
+ data = payload.get("Data")
22
+ if isinstance(data, dict):
23
+ if data.get("AlienName") and data.get("CaseID"):
24
+ return cls.OK
25
+ return cls.OTHER
26
+ message = payload.get("message") or ""
27
+ for fragment, outcome in MESSAGES:
28
+ if fragment in message:
29
+ return outcome
30
+ return cls.OTHER
31
+
32
+
33
+ MESSAGES = (
34
+ ("Invalid Captcha Provided", Outcome.CAPTCHA_REJECTED),
35
+ ("Case information is unavailable", Outcome.UNAVAILABLE),
36
+ ("No case info found", Outcome.NOT_FOUND),
37
+ ("Invalid nationality code", Outcome.INVALID_NATIONALITY),
38
+ )
acis_core/py.typed ADDED
File without changes