Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: CI

on:
push:
branches:
- main
pull_request:

jobs:
Expand Down Expand Up @@ -32,7 +29,7 @@ jobs:
run: uv sync --python ${{ matrix.python-version }}

- name: Run tests
run: uv run python -m unittest discover -s tests -p 'test_*.py'
run: make test

build:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ jobs:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Download distributions
uses: actions/download-artifact@v4
with:
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.PHONY: test

test:
uv run python -m unittest discover -s tests -p 'test_*.py'
24 changes: 24 additions & 0 deletions notamify_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from .config import ConfigStore, SDKConfig
from .models import (
ActiveNotamsQuery,
AffectedElementChangeDTO,
AffectedElementClauseDTO,
AffectedElementDTO,
AffectedElementReferenceDTO,
AffectedElementSemanticsDTO,
AircraftDetails,
BriefingJobCreated,
BriefingJobStatusDTO,
Expand All @@ -13,7 +18,10 @@
CriticalOperationalRestrictionGroup,
GenerateFlightBriefingRequest,
GenerateFlightBriefingResponse,
GroupedMeasurementValueDTO,
FractionalMeasurementValueDTO,
HistoricalNotamsQuery,
IntegerMeasurementValueDTO,
Listener,
ListenerAffectedElementFilter,
ListenerFilters,
Expand All @@ -27,6 +35,7 @@
ListenerTimeWindowFilter,
LocationType,
LocationWithType,
MeasurementComponentValueDTO,
NearbyNotamsQuery,
NotamDTO,
NotamInterpretationDTO,
Expand All @@ -37,7 +46,10 @@
NotamPriority,
NotamScheduleInterpretationDTO,
PrioritizedNotamDTO,
ProcedureCapabilityDTO,
SandboxDeliveryResult,
ValueComponentDTO,
ValueDTO,
WatcherWebhookEvent,
WebhookContext,
WebhookLifecycleChange,
Expand All @@ -53,6 +65,11 @@
__all__ = [
"APIError",
"ActiveNotamsQuery",
"AffectedElementChangeDTO",
"AffectedElementClauseDTO",
"AffectedElementDTO",
"AffectedElementReferenceDTO",
"AffectedElementSemanticsDTO",
"AircraftDetails",
"BriefingJobCreated",
"BriefingJobStatusDTO",
Expand All @@ -64,7 +81,10 @@
"CriticalOperationalRestrictionGroup",
"GenerateFlightBriefingRequest",
"GenerateFlightBriefingResponse",
"GroupedMeasurementValueDTO",
"FractionalMeasurementValueDTO",
"HistoricalNotamsQuery",
"IntegerMeasurementValueDTO",
"Listener",
"ListenerAffectedElementFilter",
"ListenerFilters",
Expand All @@ -78,6 +98,7 @@
"ListenerTimeWindowFilter",
"LocationType",
"LocationWithType",
"MeasurementComponentValueDTO",
"NearbyNotamsQuery",
"NotamDTO",
"NotamInterpretationDTO",
Expand All @@ -91,11 +112,14 @@
"NotamifyClient",
"NotamsResource",
"PrioritizedNotamDTO",
"ProcedureCapabilityDTO",
"ReceiverConfig",
"ReceivedEvent",
"SandboxDeliveryResult",
"SDKConfig",
"SignatureVerificationError",
"ValueComponentDTO",
"ValueDTO",
"WatcherWebhookEvent",
"WebhookContext",
"WebhookLifecycleChange",
Expand Down
84 changes: 84 additions & 0 deletions notamify_sdk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,95 @@ class NotamPriority(str, Enum):
low = "LOW"


class ValueComponentDTO(NotamifyModel):
type: str | None = None
value: int | str
unit: str | None = None


class ValueDTO(NotamifyModel):
kind: str | None = None
raw_string: str
values: list[ValueComponentDTO] = Field(default_factory=list)


class MeasurementComponentValueDTO(NotamifyModel):
type: str
value: int
unit: str


class IntegerMeasurementValueDTO(NotamifyModel):
kind: str | None = None
raw_string: str
value: int
unit: str


class FractionalMeasurementValueDTO(NotamifyModel):
kind: str | None = None
raw_string: str
numerator: int
denominator: int
unit: str


class GroupedMeasurementValueDTO(NotamifyModel):
kind: str | None = None
raw_string: str
values: list[MeasurementComponentValueDTO] = Field(default_factory=list)


class ProcedureCapabilityDTO(NotamifyModel):
scheme: str
category: str | None = None
level: str | None = None
classification: str | None = None
source_label: str | None = None


class AffectedElementReferenceDTO(NotamifyModel):
relation: str
type: str | None = None
identifier: str | None = None


class AffectedElementChangeDTO(NotamifyModel):
subject: str
from_: list[ProcedureCapabilityDTO | ValueDTO] = Field(default_factory=list, alias="from")
to: list[ProcedureCapabilityDTO | ValueDTO] = Field(default_factory=list)
details: str | None = None


class AffectedElementClauseDTO(NotamifyModel):
dimension: str
operator: str
value: (
IntegerMeasurementValueDTO
| FractionalMeasurementValueDTO
| GroupedMeasurementValueDTO
| ProcedureCapabilityDTO
| list[str]
)
unit: str | None = None
details: str | None = None


class AffectedElementSemanticsDTO(NotamifyModel):
scope: list[AffectedElementClauseDTO] = Field(default_factory=list)
conditions: list[AffectedElementClauseDTO] = Field(default_factory=list)
exceptions: list[AffectedElementClauseDTO] = Field(default_factory=list)
changes: list[AffectedElementChangeDTO] = Field(default_factory=list)
references: list[AffectedElementReferenceDTO] = Field(default_factory=list)


class AffectedElementDTO(NotamifyModel):
type: str
identifier: str
effect: str
details: str | None = None
subtype: str | None = None
semantics: AffectedElementSemanticsDTO | None = None


class NotamScheduleInterpretationDTO(NotamifyModel):
Expand Down
117 changes: 117 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,123 @@ def test_notam_list_result_validation(self):
result = NotamListResult.model_validate(payload)
self.assertEqual(result.notams[0].id, "n1")

def test_notam_list_result_accepts_affected_element_semantics(self):
payload = {
"notams": [
{
"id": "11111111-1111-1111-1111-111111111111",
"notam_number": "A1234/26",
"location": "KJFK",
"starts_at": "2026-02-25T10:00:00Z",
"ends_at": "2026-02-26T10:00:00Z",
"issued_at": "2026-02-25T09:00:00Z",
"is_estimated": False,
"is_permanent": False,
"message": "RWY 11 restrictions",
"interpretation": {
"description": "Runway operations restricted.",
"excerpt": "Runway 11 restrictions in effect.",
"category": "AERODROME",
"subcategory": "RUNWAY_OPERATIONS",
"affected_elements": [
{
"type": "RUNWAY",
"identifier": "11",
"effect": "RESTRICTED",
"details": "Runway 11 restricted to lighter traffic.",
"subtype": "DEPARTURE_RUNWAY",
"semantics": {
"scope": [],
"conditions": [
{
"dimension": "operation_phase",
"operator": "IN",
"value": ["TAKEOFF", "LANDING"],
"unit": None,
"details": None,
},
{
"dimension": "weight",
"operator": "LTE",
"value": {
"kind": "MEASUREMENT",
"raw_string": "5700KG",
"value": 5700,
"unit": "KG",
},
"unit": "KG",
"details": None,
},
{
"dimension": "procedure_capability",
"operator": "EQ",
"value": {
"scheme": "ILS_CATEGORY",
"category": "CAT_I",
"level": "LVL_1",
},
"unit": None,
"details": None,
},
],
"exceptions": [
{
"dimension": "aircraft_type",
"operator": "IN",
"value": ["HELICOPTER"],
"unit": None,
"details": None,
}
],
"changes": [
{
"subject": "PROCEDURE_CAPABILITY",
"from": [
{
"scheme": "ILS_CATEGORY",
"category": "CAT_II",
"level": "LVL_2",
}
],
"to": [
{
"scheme": "ILS_CATEGORY",
"category": "CAT_I",
"level": "LVL_1",
}
],
"details": "Downgraded due to maintenance.",
}
],
"references": [
{
"relation": "DEPENDS_ON",
"type": "NOTAM",
"identifier": "A1234/26",
}
],
},
}
],
"schedules": [],
},
}
],
"total_count": 1,
"page": 1,
"per_page": 30,
}

result = NotamListResult.model_validate(payload)
affected = result.notams[0].interpretation.affected_elements[0]

self.assertEqual(affected.subtype, "DEPARTURE_RUNWAY")
self.assertEqual(affected.semantics.conditions[0].value, ["TAKEOFF", "LANDING"])
self.assertEqual(affected.semantics.conditions[1].value.value, 5700)
self.assertEqual(affected.semantics.conditions[2].value.scheme, "ILS_CATEGORY")
self.assertEqual(affected.semantics.changes[0].from_[0].category, "CAT_II")
self.assertEqual(affected.semantics.references[0].relation, "DEPENDS_ON")

def test_required_fields_enforced(self):
with self.assertRaises(ValidationError):
NotamListResult.model_validate({"notams": []})
Expand Down
Loading