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 +1 -0
- acis_core/a_numbers.py +26 -0
- acis_core/exceptions.py +11 -0
- acis_core/nationalities.py +65 -0
- acis_core/outcome.py +38 -0
- acis_core/py.typed +0 -0
- acis_core/reference/nationality-codes.json +1257 -0
- acis_core-1.0.0.dist-info/METADATA +25 -0
- acis_core-1.0.0.dist-info/RECORD +11 -0
- acis_core-1.0.0.dist-info/WHEEL +4 -0
- acis_core-1.0.0.dist-info/licenses/LICENSE +661 -0
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 "***"
|
acis_core/exceptions.py
ADDED
|
@@ -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
|