diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..784c741 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "interfaces/astm/f3411/v19"] + path = interfaces/astm/f3411/v19 + url = https://github.com/uastech/standards +[submodule "interfaces/astm/f3411/v22a"] + path = interfaces/astm/f3411/v22a + url = https://github.com/uastech/standards +[submodule "interfaces/astm/f3548/v21"] + path = interfaces/astm/f3548/v21 + url = https://github.com/astm-utm/Protocol diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0a8713e --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY: apis +apis: + ./tools/openapi_conversion/generate_apis.sh diff --git a/interfaces/astm/f3411/v19 b/interfaces/astm/f3411/v19 new file mode 160000 index 0000000..36e7ea2 --- /dev/null +++ b/interfaces/astm/f3411/v19 @@ -0,0 +1 @@ +Subproject commit 36e7ea23a010ff91053f82ac4f6a9bfc698503f9 diff --git a/interfaces/astm/f3411/v22a b/interfaces/astm/f3411/v22a new file mode 160000 index 0000000..1007138 --- /dev/null +++ b/interfaces/astm/f3411/v22a @@ -0,0 +1 @@ +Subproject commit 1007138177947b3b766f76bc867daf6de3ff0b61 diff --git a/interfaces/astm/f3548/v21 b/interfaces/astm/f3548/v21 new file mode 160000 index 0000000..cb7cf96 --- /dev/null +++ b/interfaces/astm/f3548/v21 @@ -0,0 +1 @@ +Subproject commit cb7cf962d3a0c01b5ab12502f5f54789624977bf diff --git a/pyproject.toml b/pyproject.toml index 4d3d47c..1c0c779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ] -dependencies = [] +dependencies = ["implicitdict"] [project.optional-dependencies] dev = ["pytest==5.0.0", "pytest-cov[all]", "black==21.10b0"] [project.urls] diff --git a/requirements.txt b/requirements.txt index e69de29..c1d96c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +implicitdict==1.1.0 diff --git a/src/uas_standards/astm/__init__.py b/src/uas_standards/astm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/astm/f3411/__init__.py b/src/uas_standards/astm/f3411/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/astm/f3411/v19/__init__.py b/src/uas_standards/astm/f3411/v19/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/astm/f3411/v19/api.py b/src/uas_standards/astm/f3411/v19/api.py new file mode 100644 index 0000000..433c273 --- /dev/null +++ b/src/uas_standards/astm/f3411/v19/api.py @@ -0,0 +1,628 @@ +"""Data types from Discovery and Synchronization Service 1.0.0 OpenAPI""" + +# This file is autogenerated; do not modify manually! + +from __future__ import annotations + +from enum import Enum +from typing import List, Optional + +from implicitdict import ImplicitDict, StringBasedDateTime + + +URL = str +"""Valid http or https URL.""" + + +SubscriptionNotificationIndex = int +"""Tracks the notifications sent for a subscription so the subscriber can detect missed notifications more easily.""" + + +UUIDv4 = str +"""UUID v4.""" + + +Version = str +"""A version string used to reference an object at a particular point in time. Any updates to an object must contain the corresponding version to maintain idempotent updates.""" + + +EntityUUID = UUIDv4 + + +SubscriptionUUID = UUIDv4 + + +RIDFlightID = str +"""ID, unique to a remote ID service provider, which identifies a particular flight for which the remote ID service provider is providing remote ID services. + +The following characters are not allowed: \0 \t \r \n # % / : ? @ [ \ ] +""" + + +class RIDAuthData(ImplicitDict): + """Additional authentication data.""" + + format: str + """Format of additional authentication data.""" + + data: str + """Authentication data in form specified by `format`.""" + + +class HorizontalAccuracy(str, Enum): + """This is the NACp enumeration from ADS-B, plus 1m for a more complete range for UAs. + + `HAUnknown`: Unknown horizontal accuracy + + `HA10NMPlus`: > 10NM (18.52km) + + `HA10NM`: < 10NM (18.52km) + + `HA4NM`: < 4NM (7.408km) + + `HA2NM`: < 2NM (3.704km) + + `HA1NM`: < 1NM (1852m) + + `HA05NM`: < 0.5NM (926m) + + `HA03NM`: < 0.3NM (555.6m) + + `HA01NM`: < 0.1NM (185.2m) + + `HA005NM`: < 0.05NM (92.6m) + + `HA30m`: < 30m + + `HA10m`: < 10m + + `HA3m`: < 3m + + `HA1m`: < 1m + """ + + HAUnknown = "HAUnknown" + HA10NMPlus = "HA10NMPlus" + HA10NM = "HA10NM" + HA4NM = "HA4NM" + HA2NM = "HA2NM" + HA1NM = "HA1NM" + HA05NM = "HA05NM" + HA03NM = "HA03NM" + HA01NM = "HA01NM" + HA005NM = "HA005NM" + HA30m = "HA30m" + HA10m = "HA10m" + HA3m = "HA3m" + HA1m = "HA1m" + + +class VerticalAccuracy(str, Enum): + """This is the GVA enumeration from ADS-B, plus some finer values for UAs. + + `VAUnknown`: Unknown vertical accuracy + + `VA150mPlus`: > 150m + + `VA150m`: < 150m + + `VA45m`: < 45m + + `VA25m`: < 25m + + `VA10m`: < 10m + + `VA3m`: < 3m + + `VA1m`: < 1m + """ + + VAUnknown = "VAUnknown" + VA150mPlus = "VA150mPlus" + VA150m = "VA150m" + VA45m = "VA45m" + VA25m = "VA25m" + VA10m = "VA10m" + VA3m = "VA3m" + VA1m = "VA1m" + + +class ErrorResponse(ImplicitDict): + """Data provided when an off-nominal condition was encountered.""" + + message: Optional[str] + """Human-readable message indicating what error occurred and/or why.""" + + +class SpeedAccuracy(str, Enum): + """This is the same enumeration scale and values from ADS-B NACv. + + `SAUnknown`: Unknown speed accuracy + + `SA10mpsPlus`: > 10 m/s + + `SA10mps`: < 10 m/s + + `SA3mps`: < 3 m/s + + `SA1mps`: < 1 m/s + + `SA03mps`: < 0.3 m/s + """ + + SAUnknown = "SAUnknown" + SA10mpsPlus = "SA10mpsPlus" + SA10mps = "SA10mps" + SA3mps = "SA3mps" + SA1mps = "SA1mps" + SA03mps = "SA03mps" + + +GeoPolygonString = str +"""Plain-string representation of a geographic polygon consisting of at least three geographic points describing a closed polygon on the earth. Each point consists of latitude,longitude in degrees. Points are also comma-delimited, so this parameter will look like `lat1,lng1,lat2,lng2,lat3,lng3,...` Latitude values must fall in the range [-90, 90] and longitude values must fall in the range [-180, 180]. + +All of the requirements and clarifications for GeoPolygon apply to GeoPolygonString as well. +""" + + +class RIDHeightReference(str, Enum): + """The reference datum above which the height is reported.""" + + TakeoffLocation = "TakeoffLocation" + GroundLevel = "GroundLevel" + + +class RIDHeight(ImplicitDict): + """A relative altitude for the purposes of remote ID.""" + + distance: float + """Distance above reference datum. This value is provided in meters and must have a minimum resolution of 1 meter.""" + + reference: RIDHeightReference + """The reference datum above which the height is reported.""" + + +Latitude = float +"""Degrees of latitude north of the equator, with reference to the WGS84 ellipsoid.""" + + +Longitude = float +"""Degrees of longitude east of the Prime Meridian, with reference to the WGS84 ellipsoid.""" + + +class LatLngPoint(ImplicitDict): + """Point on the earth's surface.""" + + lng: Longitude + + lat: Latitude + + +Altitude = float +"""An altitude, in meters, above the WGS84 ellipsoid.""" + + +class GeoPolygon(ImplicitDict): + """An enclosed area on the earth. + The bounding edges of this polygon shall be the shortest paths between connected vertices. This means, for instance, that the edge between two points both defined at a particular latitude is not generally contained at that latitude. + The winding order shall be interpreted as the order which produces the smaller area. + The path between two vertices shall be the shortest possible path between those vertices. + Edges may not cross. + Vertices may not be duplicated. In particular, the final polygon vertex shall not be identical to the first vertex. + """ + + vertices: List[LatLngPoint] + + +IdentificationServiceAreaURL = str +"""The URL at which notifications regarding Identification Service Areas may be delivered. See the `/uss/identification_service_areas/{id}` path for specification of this endpoint.""" + + +class SubscriptionCallbacks(ImplicitDict): + """Endpoints that should be called when an applicable event occurs. At least one field must be specified.""" + + identification_service_area_url: Optional[IdentificationServiceAreaURL] + """If specified, other clients will be instructed by the DSS to call this endpoint when an Identification Service Area relevant to this Subscription is created, modified, or deleted. Must implement PUT and DELETE according to the `/uss/identification_service_areas/{id}` path API.""" + + +class RIDOperationalStatus(str, Enum): + """Indicates operational status of associated aircraft.""" + + Undeclared = "Undeclared" + Ground = "Ground" + Airborne = "Airborne" + + +class RIDFlightDetails(ImplicitDict): + """Details about a flight reported by a remote ID service provider. At least one of the registration or serial fields must be filled if required by CAA.""" + + id: str + """ID for this flight, matching argument in request.""" + + operator_id: Optional[str] + """CAA-issued registration/license ID for the remote pilot or operator. """ + + operator_location: Optional[LatLngPoint] + """Location of party controlling the aircraft.""" + + operation_description: Optional[str] + """Free-text field that enables the operator to describe the purpose of a flight, if so desired.""" + + auth_data: Optional[RIDAuthData] + + serial_number: Optional[str] + """Can be specified when no registration ID exists and required by law in a region. This is expressed in the ANSI/CTA-2063-A Physical Serial Number format.""" + + registration_number: Optional[str] + """If a CAA provides a method of registering UAS, this number is provided by the CAA or its authorized representative. Required when required by law in a region.""" + + +class Subscription(ImplicitDict): + """Specification of a geographic area that a client is interested in on an ongoing basis (e.g., “planning area”). Internal to the DSS.""" + + id: SubscriptionUUID + """Unique identifier for this subscription.""" + + callbacks: SubscriptionCallbacks + + owner: str + """Assigned by the DSS based on creating client’s ID (via access token). Used for restricting mutation and deletion operations to owner.""" + + notification_index: SubscriptionNotificationIndex + + time_end: Optional[StringBasedDateTime] + """If set, this subscription will be automatically removed after this time. RFC 3339 format, per OpenAPI specification.""" + + time_start: Optional[StringBasedDateTime] + """If set, this Subscription will not generate any notifications before this time. RFC 3339 format, per OpenAPI specification.""" + + version: Version + + +class RIDAircraftType(str, Enum): + """Type of aircraft for the purposes of remote ID. + + `VTOL` is a fixed wing aircraft that can take off vertically. `Helicopter` includes multirotor. + """ + + NotDeclared = "NotDeclared" + Aeroplane = "Aeroplane" + Helicopter = "Helicopter" + Gyroplane = "Gyroplane" + VTOL = "VTOL" + Ornithopter = "Ornithopter" + Glider = "Glider" + Kite = "Kite" + FreeBalloon = "FreeBalloon" + CaptiveBalloon = "CaptiveBalloon" + Airship = "Airship" + FreeFallOrParachute = "FreeFallOrParachute" + Rocket = "Rocket" + TetheredPoweredAircraft = "TetheredPoweredAircraft" + GroundObstacle = "GroundObstacle" + Other = "Other" + + +RIDFlightsURL = str +"""The URL at which the remote ID flights and their details may be retrieved. See `/flights` and `/flights/{id}/details` paths for specification of this endpoint. +This URL is the base flights resource. If this URL is specified as https://example.com/flights then the flight details for a particular {id} may be obtained at the URL https://example.com/flights/{id}/details. This URL may not have a trailing / character. +""" + + +class Volume3D(ImplicitDict): + """A three-dimensional geographic volume consisting of a vertically-extruded polygon.""" + + footprint: GeoPolygon + """Projection of this volume onto the earth's surface.""" + + altitude_lo: Optional[Altitude] + """Minimum bounding altitude of this volume.""" + + altitude_hi: Optional[Altitude] + """Maximum bounding altitude of this volume.""" + + +class Volume4D(ImplicitDict): + """Contiguous block of geographic spacetime.""" + + spatial_volume: Volume3D + """Constant spatial extent of this volume.""" + + time_start: Optional[StringBasedDateTime] + """Beginning time of this volume. RFC 3339 format, per OpenAPI specification.""" + + time_end: Optional[StringBasedDateTime] + """End time of this volume. RFC 3339 format, per OpenAPI specification.""" + + +class GetSubscriptionResponse(ImplicitDict): + """Response to DSS request for the subscription with the given id.""" + + subscription: Subscription + + +class SearchSubscriptionsResponse(ImplicitDict): + """Response to DSS query for subscriptions in a particular area.""" + + subscriptions: List[Subscription] + """Subscriptions that overlap the specified area.""" + + +class SubscriptionState(ImplicitDict): + """State of AreaSubscription which is causing a notification to be sent.""" + + subscription_id: Optional[SubscriptionUUID] + + notification_index: Optional[SubscriptionNotificationIndex] + + +class GetFlightDetailsResponse(ImplicitDict): + """Response to remote ID provider query for details about a specific flight.""" + + details: RIDFlightDetails + + +class DeleteSubscriptionResponse(ImplicitDict): + """Response for a successful request to delete an Subscription.""" + + subscription: Subscription + """The Subscription which was deleted.""" + + +class RIDAircraftPosition(ImplicitDict): + """Position of an aircraft as reported for remote ID purposes.""" + + lat: Latitude + + lng: Longitude + + alt: float + """Geodetic altitude (NOT altitude above launch, altitude above ground, or EGM96): aircraft distance above the WGS84 ellipsoid as measured along a line that passes through the aircraft and is normal to the surface of the WGS84 ellipsoid. This value is provided in meters and must have a minimum resolution of 1 meter.""" + + accuracy_h: Optional[HorizontalAccuracy] + """Horizontal error that is likely to be present in this reported position. Required when `extrapolated` field is true and always in the entry for the current state.""" + + accuracy_v: Optional[VerticalAccuracy] + """Vertical error that is likely to be present in this reported position. Required when `extrapolated` field is true and always in the entry for the current state.""" + + extrapolated: Optional[bool] + """True if this position was generated primarily by computation rather than primarily from a direct instrument measurement. Assumed false if not specified.""" + + pressure_altitude: Optional[float] + """The uncorrected altitude (based on reference standard 29.92 inHg, 1013.25 mb) provides a reference for algorithms that utilize "altitude deltas" between aircraft. This value is provided in meters and must have a minimum resolution of 1 meter.""" + + +class SubscriberToNotify(ImplicitDict): + """Subscriber to notify of a creation/change/deletion of a change in the airspace. This is provided by the DSS to a client changing the airspace, and it is the responsibility of the client changing the airspace (they will receive a set of these notification requests) to send a notification to each specified `url`.""" + + subscriptions: List[SubscriptionState] + """Subscription(s) prompting this notification.""" + + url: URL + """The endpoint that the client mutating the airspace should provide the update to. API depends on the DSS action taken that triggered this notification request.""" + + +class RIDRecentAircraftPosition(ImplicitDict): + time: StringBasedDateTime + """Time at which this position applied. RFC 3339 format, per OpenAPI specification.""" + + position: RIDAircraftPosition + + +class CreateIdentificationServiceAreaParameters(ImplicitDict): + """Parameters for a request to create an Identification Service Area in the DSS.""" + + extents: Volume4D + """The bounding spacetime extents of this Identification Service Area. End time must be specified. If start time is not specified, it will be set to the current time. Start times in the past should be rejected by the DSS, except that it may adjust very recent start times to the current time. + + These extents should not reveal any sensitive information about the flight or flights within them. This means, for instance, that extents should not tightly-wrap a flight path, nor should they generally be centered around the takeoff point of a single flight. + """ + + flights_url: RIDFlightsURL + + +class UpdateIdentificationServiceAreaParameters(ImplicitDict): + """Parameters for a request to update an Identification Service Area in the DSS.""" + + extents: Volume4D + """The bounding spacetime extents of this Identification Service Area. End time must be specified. If start time is not specified, it will remain unchanged. Start times in the past should be rejected by the DSS unless they are unchanged from the Identification Service Area's current start time. + + These extents should not reveal any sensitive information about the flight or flights within them. This means, for instance, that extents should not tightly-wrap a flight path, nor should they generally be centered around the takeoff point of a single flight. + """ + + flights_url: RIDFlightsURL + + +class CreateSubscriptionParameters(ImplicitDict): + """Parameters for a request to create a subscription in the DSS.""" + + extents: Volume4D + """The spacetime extents of the volume to subscribe to. + + This subscription will automatically be deleted after its end time if it has not been refreshed by then. If end time is not specified, the value will be chosen automatically by the DSS. + + Note that some Entities triggering notifications may lie entirely outside the requested area. + """ + + callbacks: SubscriptionCallbacks + + +class UpdateSubscriptionParameters(ImplicitDict): + """Parameters for a request to update a subscription in the DSS.""" + + extents: Volume4D + """The spacetime extents of the volume to subscribe to. + + This subscription will automatically be deleted after its end time if it has not been refreshed by then. If end time is not specified, the value will be chosen automatically by the DSS. + + Note that some Entities triggering notifications may lie entirely outside the requested area. + """ + + callbacks: SubscriptionCallbacks + + +class IdentificationServiceArea(ImplicitDict): + """An Identification Service Area (area in which remote ID services are being provided). The DSS reports only these declarations and clients must exchange flight information peer-to-peer.""" + + flights_url: RIDFlightsURL + + owner: str + """Assigned by the DSS based on creating client’s ID (via access token). Used for restricting mutation and deletion operations to owner.""" + + time_start: StringBasedDateTime + """Beginning time of service. RFC 3339 format, per OpenAPI specification.""" + + time_end: StringBasedDateTime + """End time of service. RFC 3339 format, per OpenAPI specification.""" + + version: Version + + id: EntityUUID + """Unique identifier for this Identification Service Area.""" + + +class RIDAircraftState(ImplicitDict): + """State of an aircraft for the purposes of remote ID.""" + + timestamp: StringBasedDateTime + """Time at which this state was valid. This may be the time coming from the source, such as a GPS, or the time when the system computes the values using an algorithm such as an Extended Kalman Filter (EKF). Timestamp must be expressed with a minimum resolution of 1/10th of a second. RFC 3339 format, per OpenAPI specification.""" + + timestamp_accuracy: float + """Declaration of timestamp accuracy, which is the largest difference between Timestamp and true time of applicability for any of the following fields: Latitude, Longitude, Geodetic Altitude, Pressure Altitude of Position, Height. to determine time of applicability of the location data provided. Expressed in seconds, precise to 1/10ths of seconds. The accuracy reflects the 95% uncertainty bound value for the timestamp.""" + + operational_status: Optional[RIDOperationalStatus] + + position: RIDAircraftPosition + + track: float + """Direction of flight expressed as a "True North-based" ground track angle. This value is provided in degrees East of North with a minimum resolution of 1 degree.""" + + speed: float + """Ground speed of flight in meters per second.""" + + speed_accuracy: SpeedAccuracy + """Accuracy of horizontal ground speed.""" + + vertical_speed: float + """Speed up (vertically) WGS84-HAE, m/s.""" + + height: Optional[RIDHeight] + + group_radius: Optional[float] + """Farthest horizontal distance from reported group location at which an aircraft in the group may be located (meters). This value contains the "Operating Area Radius" data from the common data dictionary when group operation area is specified by point-radius.""" + + group_ceiling: Optional[float] + """Maximum altitude (meters WGS84-HAE) of Group Operation. This value contains the "Operating Area Ceiling" data from the common data dictionary when group operation area is specified by point-radius.""" + + group_floor: Optional[float] + """Minimum altitude (meters WGS84-HAE) of Group Operation. If not specified, ground level shall be assumed. This value contains the "Operating Area Floor" data from the common data dictionary when group operation area is specified by point-radius.""" + + group_count: Optional[int] + """When operating a group (or formation or swarm), number of aircraft in group. This value contains the "Operating Area Count" data from the common data dictionary when group operation area is specified by point-radius.""" + + group_time_start: Optional[StringBasedDateTime] + """Time at which a group operation starts. This value contains the "Operation Area Start" data from the common data dictionary when group operation area is specified by point-radius.""" + + group_time_end: Optional[StringBasedDateTime] + """Time at which a group operation starts. This value contains the "Operation Area End" data from the common data dictionary when group operation area is specified by point-radius.""" + + +class RIDFlight(ImplicitDict): + """Description of a remote ID flight.""" + + id: RIDFlightID + + aircraft_type: RIDAircraftType + """Generic type of aircraft.""" + + current_state: Optional[RIDAircraftState] + """The most up-to-date state of the aircraft. Required when the aircraft is currently in the requested area unless `volumes` is specified. + + If current data is not being received from the UAS by the Service Provider, the lack of change in this field is sufficient to indicate that current data is not being received. + """ + + volumes: Optional[List[Volume4D]] + """The set of spacetime volumes the aircraft is within. Required if `current_state` is not specified. The fields `time_start` and `time_end` are required if `current_state` is not specified.""" + + simulated: Optional[bool] + """If specified as true, this flight is not a physical aircraft; it is just a simulation to test the system.""" + + recent_positions: Optional[List[RIDRecentAircraftPosition]] + """A short collection of recent aircraft movement, specified only when `include_recent_positions` is true. If `volumes` is not specified and `include_recent_positions` is true, then this field is required. + + Recent positions provided in this field must conform to requirements in the standard which generally prohibit including positions outside the requested area except transitionally when the aircraft enters or exits the requested area, and which prohibit including positions that not sufficiently recent. + + Note that a UI should not draw a connective line between two consecutive position reports that both lie outside the requested area. + """ + + +class PutIdentificationServiceAreaResponse(ImplicitDict): + """Response to a request to create or update a reference to an Identification Service Area in the DSS.""" + + subscribers: List[SubscriberToNotify] + """DSS subscribers that this client now has the obligation to notify of the Identification Service Area changes just made. This client must call POST for each provided URL according to the `/uss/identification_service_areas/{id}` path API.""" + + service_area: IdentificationServiceArea + """Resulting service area stored in DSS.""" + + +class SearchIdentificationServiceAreasResponse(ImplicitDict): + """Response to DSS query for Identification Service Areas in an area of interest.""" + + service_areas: List[IdentificationServiceArea] + """Identification Service Areas in the area of interest.""" + + +class PutIdentificationServiceAreaNotificationParameters(ImplicitDict): + """Parameters of a message informing of new full information for an Identification Service Area. Pushed (by a client, not the DSS) directly to clients with subscriptions when another client makes a change to airspace within a cell with a subscription.""" + + service_area: Optional[IdentificationServiceArea] + """Identification Service Area that the notifying client changed or created. + + If this field is populated, the Identification Service Area was created or updated. If this field is not populated, the Identification Service Area was deleted. + """ + + subscriptions: List[SubscriptionState] + """Subscription(s) prompting this notification.""" + + extents: Optional[Volume4D] + """The new or updated extents of the Identification Service Area. + + Omitted if Identification Service Area was deleted. + """ + + +class DeleteIdentificationServiceAreaResponse(ImplicitDict): + """Response for a request to delete an Identification Service Area.""" + + service_area: IdentificationServiceArea + """Indentification Service Area that was just deleted.""" + + subscribers: List[SubscriberToNotify] + """DSS subscribers that this client now has the obligation to notify of the Identification Service Area just deleted. This client must call POST for each provided URL according to the `/uss/identification_service_areas` path API.""" + + +class PutSubscriptionResponse(ImplicitDict): + """Response for a request to create or update a subscription.""" + + service_areas: Optional[List[IdentificationServiceArea]] + """Identification Service Areas in or near the subscription area at the time of creation/update, if `identification_service_area_url` callback was specified.""" + + subscription: Subscription + """Result of the operation on the subscription.""" + + +class GetIdentificationServiceAreaResponse(ImplicitDict): + """Response to DSS request for the identification service area with the given id.""" + + service_area: IdentificationServiceArea + + +class GetFlightsResponse(ImplicitDict): + """Response to remote ID provider query for flight information in an area of interest.""" + + timestamp: StringBasedDateTime + """The remote ID service provider's timestamp for when this information was current. RFC 3339 format, per OpenAPI specification.""" + + flights: List[RIDFlight] + """A list of all flights that have been within the requested area within the remote ID retention period. This includes flights that are not currently within the requested area, but were within the requested area within the remote ID retention period.""" diff --git a/src/uas_standards/astm/f3411/v22a/__init__.py b/src/uas_standards/astm/f3411/v22a/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/astm/f3411/v22a/api.py b/src/uas_standards/astm/f3411/v22a/api.py new file mode 100644 index 0000000..77eedbc --- /dev/null +++ b/src/uas_standards/astm/f3411/v22a/api.py @@ -0,0 +1,792 @@ +"""Data types from Standard Remote ID API Interfaces 2.1.0 OpenAPI""" + +# This file is autogenerated; do not modify manually! + +from __future__ import annotations + +from enum import Enum +from typing import List, Optional + +from implicitdict import ImplicitDict, StringBasedDateTime + + +class TimeFormat(str, Enum): + RFC3339 = "RFC3339" + + +class Time(ImplicitDict): + value: StringBasedDateTime + """RFC3339-formatted time/date string. The time zone must be 'Z'.""" + + format: TimeFormat = TimeFormat.RFC3339 + + +class RadiusUnits(str, Enum): + """FIXM-compatible units. Only meters ("M") are acceptable for UTM.""" + + M = "M" + + +class Radius(ImplicitDict): + value: float + """Distance from the centerpoint of a circular area, along the WGS84 ellipsoid.""" + + units: RadiusUnits = RadiusUnits.M + """FIXM-compatible units. Only meters ("M") are acceptable for UTM.""" + + +URL = str +"""Valid http or https URL.""" + + +SubscriptionNotificationIndex = int +"""Tracks the notifications sent for a subscription so the subscriber can detect missed notifications more easily.""" + + +UUIDv4 = str +"""UUID v4.""" + + +Version = str +"""A version string used to reference an object at a particular point in time. Any updates to an object must contain the corresponding version to maintain idempotent updates.""" + + +EntityUUID = UUIDv4 + + +SubscriptionUUID = UUIDv4 + + +RIDFlightID = str +"""ID, unique to a remote ID service provider, which identifies a particular flight for which the remote ID service provider is providing remote ID services. + +The following characters are not allowed: \0 \t \r \n # % / : ? @ [ \ ] +""" + + +class RIDAuthData(ImplicitDict): + """Additional authentication data.""" + + format: Optional[int] = 0 + """Format of additional authentication data. + + 0: None + 1: UAS ID Signature + 2: Operator ID Signature + 3: Message Set Signature + 4: Authentication Provided by Network Remote ID + 5: Specific Method + 6-9: Reserved for Spec + 10-15: Available for Private Use + """ + + data: Optional[str] = "" + """Authentication data in form specified by `format`.""" + + +class HorizontalAccuracy(str, Enum): + """This is based on ADS-B NACp (plus the one extra increment). + + `HAUnknown`: Unknown horizontal accuracy + + `HA10NMPlus`: > 10NM (18.52km) + + `HA10NM`: < 10NM (18.52km) + + `HA4NM`: < 4NM (7.408km) + + `HA2NM`: < 2NM (3.704km) + + `HA1NM`: < 1NM (1852m) + + `HA05NM`: < 0.5NM (926m) + + `HA03NM`: < 0.3NM (555.6m) + + `HA01NM`: < 0.1NM (185.2m) + + `HA005NM`: < 0.05NM (92.6m) + + `HA30m`: < 30m + + `HA10m`: < 10m + + `HA3m`: < 3m + + `HA1m`: < 1m + """ + + HAUnknown = "HAUnknown" + HA10NMPlus = "HA10NMPlus" + HA10NM = "HA10NM" + HA4NM = "HA4NM" + HA2NM = "HA2NM" + HA1NM = "HA1NM" + HA05NM = "HA05NM" + HA03NM = "HA03NM" + HA01NM = "HA01NM" + HA005NM = "HA005NM" + HA30m = "HA30m" + HA10m = "HA10m" + HA3m = "HA3m" + HA1m = "HA1m" + + +class VerticalAccuracy(str, Enum): + """This is based on ADS-B Geodetic Vertical Accuracy (GVA) (plus the three extra increments) + + `VAUnknown`: Unknown vertical accuracy + + `VA150mPlus`: > 150m + + `VA150m`: < 150m + + `VA45m`: < 45m + + `VA25m`: < 25m + + `VA10m`: < 10m + + `VA3m`: < 3m + + `VA1m`: < 1m + """ + + VAUnknown = "VAUnknown" + VA150mPlus = "VA150mPlus" + VA150m = "VA150m" + VA45m = "VA45m" + VA25m = "VA25m" + VA10m = "VA10m" + VA3m = "VA3m" + VA1m = "VA1m" + + +class ErrorResponse(ImplicitDict): + """Data provided when an off-nominal condition was encountered.""" + + message: Optional[str] = "" + """Human-readable message indicating what error occurred and/or why.""" + + +class SpeedAccuracy(str, Enum): + """Provides quality/containment on horizontal ground speed. + + `SAUnknown`: Unknown speed accuracy + + `SA10mpsPlus`: > 10 m/s + + `SA10mps`: < 10 m/s + + `SA3mps`: < 3 m/s + + `SA1mps`: < 1 m/s + + `SA03mps`: < 0.3 m/s + """ + + SAUnknown = "SAUnknown" + SA10mpsPlus = "SA10mpsPlus" + SA10mps = "SA10mps" + SA3mps = "SA3mps" + SA1mps = "SA1mps" + SA03mps = "SA03mps" + + +GeoPolygonString = str +"""Plain-string representation of a geographic polygon consisting of at least three geographic +points describing a closed polygon on the earth. Each point consists of latitude,longitude +in degrees. Points are also comma-delimited, so this parameter will look like +`lat1,lng1,lat2,lng2,lat3,lng3,...` Latitude values must fall in the range [-90, 90] and +longitude values must fall in the range [-180, 180]. + +All of the requirements and clarifications for Polygon apply to GeoPolygonString as well. +""" + + +class RIDHeightReference(str, Enum): + """The reference datum above which the height is reported.""" + + TakeoffLocation = "TakeoffLocation" + GroundLevel = "GroundLevel" + + +class RIDHeight(ImplicitDict): + """A relative altitude for the purposes of remote ID.""" + + distance: Optional[float] = 0 + """Distance above reference datum. This value is provided in meters and must have a minimum resolution of 1 meter. Invalid, No Value or Unknown is -1000 m.""" + + reference: RIDHeightReference + """The reference datum above which the height is reported.""" + + +Latitude = float +"""Degrees of latitude north of the equator, with reference to the WGS84 ellipsoid. Invalid, No Value, or Unknown is 0 degrees (both Lat/Lon).""" + + +Longitude = float +"""Degrees of longitude east of the Prime Meridian, with reference to the WGS84 ellipsoid. Invalid, No Value, or Unknown is 0 degrees (both Lat/Lon).""" + + +class LatLngPoint(ImplicitDict): + """Point on the earth's surface.""" + + lng: Longitude + + lat: Latitude + + +class AltitudeReference(str, Enum): + """A code indicating the reference for a vertical distance. See AIXM 5.1 and FIXM 4.2.0. Currently, UTM only allows WGS84 with no immediate plans to allow other options. FIXM and AIXM allow for 'SFC' which is equivalent to AGL.""" + + W84 = "W84" + + +class AltitudeUnits(str, Enum): + """The reference quantities used to express the value of altitude. See FIXM 4.2. Currently, UTM only allows meters with no immediate plans to allow other options.""" + + M = "M" + + +class Altitude(ImplicitDict): + value: float + """The numeric value of the altitude. Note that min and max values are added as a sanity check. As use cases evolve and more options are made available in terms of units of measure or reference systems, these bounds may be re-evaluated. Invalid, No Value, or Unknown is –1000 m.""" + + reference: AltitudeReference = AltitudeReference.W84 + """A code indicating the reference for a vertical distance. See AIXM 5.1 and FIXM 4.2.0. Currently, UTM only allows WGS84 with no immediate plans to allow other options. FIXM and AIXM allow for 'SFC' which is equivalent to AGL.""" + + units: AltitudeUnits = AltitudeUnits.M + """The reference quantities used to express the value of altitude. See FIXM 4.2. Currently, UTM only allows meters with no immediate plans to allow other options.""" + + +class Polygon(ImplicitDict): + """An enclosed area on the earth. The bounding edges of this polygon are defined to be the shortest paths between connected vertices. This means, for instance, that the edge between two points both defined at a particular latitude is not generally contained at that latitude. The winding order must be interpreted as the order which produces the smaller area. The path between two vertices is defined to be the shortest possible path between those vertices. Edges may not cross. Vertices may not be duplicated. In particular, the final polygon vertex must not be identical to the first vertex.""" + + vertices: List[LatLngPoint] + + +class RIDOperationalStatus(str, Enum): + """Operational Status indicates whether the associated UA is on the ground, airborne, or in an + emergency situation, or the Remote ID system has failed. The emergency status takes + precedence over the other status modes. This status can be used for filtering purposes. + """ + + Undeclared = "Undeclared" + Ground = "Ground" + Airborne = "Airborne" + Emergency = "Emergency" + RemoteIDSystemFailure = "RemoteIDSystemFailure" + + +SpecificSessionID = str +"""A unique 20 byte ID intended to identify a specific flight (session) while providing a +greater level of privacy to the operator. The first byte is the registered unique Specific Session ID +Type maintained by a registrar (see Annex A5), and the remaining 19 bytes is the Session ID per the +Specific Session ID Type specification. + +Initial scheme registry entries include: + +0 – reserved +1 – Internet Engineering Task Force (IETF) Drone Remote Identification Protocol (DRIP) entity ID +2 – IEEE 1609.2 HashedID8 + +Expressed as a hexadecimal string of the underlying data bytes. A recipient should ignore any dashes in this value. If fewer than 20 bytes are specified, the remaining bytes at the end are assumed to have value 0. +""" + + +class UASID(ImplicitDict): + """Identification of the UAS performing this flight. At least one field of this object must be specified.""" + + serial_number: Optional[str] = "" + """This is expressed in the CTA-2063-A Serial Number format.""" + + registration_id: Optional[str] = "" + """If a CAA provides a method of registering UAS, this number is provided by the CAA or its authorized representative. Format is ., ASCII encoded, only uppercase letters (A-Z), dot (.), and digits (0-9) are allowed. Example is valid for the US.""" + + utm_id: Optional[str] = "" + """A UTM-provided universally unique ID traceable to a non-obfuscated ID that acts as a "session id" to protect exposure of operationally sensitive information.""" + + specific_session_id: Optional[SpecificSessionID] + + +class OperatorLocationAltitude_type(str, Enum): + """Source of data for the altitude field.""" + + Takeoff = "Takeoff" + Dynamic = "Dynamic" + Fixed = "Fixed" + + +class OperatorLocation(ImplicitDict): + """Location associated with the Remote Pilot""" + + position: LatLngPoint + """Position of the Remote Pilot.""" + + altitude: Optional[Altitude] + """Provides the Operator Altitude based on WGS-84 height above ellipsoid (HAE) (See Geodetic Altitude). This value is provided in meters and must have a minimum resolution of 1 m.""" + + altitude_type: Optional[OperatorLocationAltitude_type] + """Source of data for the altitude field.""" + + +class UAClassificationEUCategory(str, Enum): + EUCategoryUndefined = "EUCategoryUndefined" + Open = "Open" + Specific = "Specific" + Certified = "Certified" + + +class UAClassificationEUClass(str, Enum): + EUClassUndefined = "EUClassUndefined" + Class0 = "Class0" + Class1 = "Class1" + Class2 = "Class2" + Class3 = "Class3" + Class4 = "Class4" + Class5 = "Class5" + Class6 = "Class6" + + +UAClassificationEU = dict +"""Expected keys: +* category +* class +""" + + + +class RIDFlightDetails(ImplicitDict): + """Details about a flight reported by a remote ID service provider. At least one of the registration or serial fields must be filled if required by CAA.""" + + id: str + """ID for this flight, matching argument in request.""" + + eu_classification: Optional[UAClassificationEU] + """When this field is specified, the Classification Type is "European Union". If no other classification field is specified, the Classification Type is "Undeclared".""" + + uas_id: Optional[UASID] + + operator_id: Optional[str] = "" + """This optional field provides a CAA-issued registration/license ID for the remote pilot or operator. Format is ., ASCII encoded, only uppercase letters (A-Z), dot (.), and digits (0-9) are allowed. Example is valid for the US.""" + + operator_location: Optional[OperatorLocation] + + operation_description: Optional[str] = "" + """Free-text field that enables the operator to describe the purpose of a flight, if so desired.""" + + auth_data: Optional[RIDAuthData] + + +class UAType(str, Enum): + """The UA Type can help infer performance, speed, and duration of flights, for example, a + "fixed wing" can generally fly in a forward direction only (as compared to a multi-rotor). + This can also help differentiate aircraft types without sharing operationally sensitive + information like the make and model of a particular aircraft. Make and model are + anticipated to become available during the Registration ID lookup process. UAS Type is also + useful for correlating visual observation with data received. The types were formulated + based on unique flight characteristics. + + `HybridLift` is a fixed wing aircraft that can take off vertically. `Helicopter` includes multirotor. + """ + + NotDeclared = "NotDeclared" + Aeroplane = "Aeroplane" + Helicopter = "Helicopter" + Gyroplane = "Gyroplane" + HybridLift = "HybridLift" + Ornithopter = "Ornithopter" + Glider = "Glider" + Kite = "Kite" + FreeBalloon = "FreeBalloon" + CaptiveBalloon = "CaptiveBalloon" + Airship = "Airship" + FreeFallOrParachute = "FreeFallOrParachute" + Rocket = "Rocket" + TetheredPoweredAircraft = "TetheredPoweredAircraft" + GroundObstacle = "GroundObstacle" + Other = "Other" + + +USSBaseURL = str +"""The base URL of a USS implementation of part or all of the remote ID USS-USS API. Per the USS-USS API, the full URL +of a particular resource can be constructed by appending, e.g., `/uss/{resource}/{id}` to this base URL. +Accordingly, this URL may not have a trailing '/' character. +""" + + +SubscriptionUSSBaseURL = USSBaseURL + + +FlightsUSSBaseURL = USSBaseURL + + +class Circle(ImplicitDict): + """A circular area on the surface of the earth.""" + + center: Optional[LatLngPoint] + + radius: Optional[Radius] + + +class Volume3D(ImplicitDict): + """A three-dimensional geographic volume consisting of a vertically-extruded shape. Exactly one outline must be specified.""" + + outline_circle: Optional[Circle] + """A circular geographic shape on the surface of the earth.""" + + outline_polygon: Optional[Polygon] + """A polygonal geographic shape on the surface of the earth.""" + + altitude_lower: Optional[Altitude] + """Minimum bounding altitude of this volume. Must be less than altitude_upper, if specified.""" + + altitude_upper: Optional[Altitude] + """Maximum bounding altitude of this volume. Must be greater than altitude_lower, if specified.""" + + +class Volume4D(ImplicitDict): + """Contiguous block of geographic spacetime.""" + + volume: Volume3D + + time_start: Optional[Time] + """Beginning time of this volume. Must be before time_end.""" + + time_end: Optional[Time] + """End time of this volume. Must be after time_start.""" + + +class SubscriptionState(ImplicitDict): + """State of Subscription which is causing a notification to be sent.""" + + subscription_id: SubscriptionUUID + + notification_index: Optional[SubscriptionNotificationIndex] = 0 + + +class GetFlightDetailsResponse(ImplicitDict): + """Response to remote ID provider query for details about a specific flight.""" + + details: RIDFlightDetails + + +class RIDAircraftPosition(ImplicitDict): + """Position of an aircraft as reported for remote ID purposes.""" + + lat: Optional[Latitude] + + lng: Optional[Longitude] + + alt: Optional[float] = -1000 + """Geodetic altitude (NOT altitude above launch, altitude above ground, or EGM96): aircraft distance above the WGS84 ellipsoid as measured along a line that passes through the aircraft and is normal to the surface of the WGS84 ellipsoid. This value is provided in meters and must have a minimum resolution of 1 meter. Invalid, No Value or Unknown is -1000 m.""" + + accuracy_h: Optional[HorizontalAccuracy] + """Horizontal error that is likely to be present in this reported position. Required when `extrapolated` field is true and always in the entry for the current state.""" + + accuracy_v: Optional[VerticalAccuracy] + """Vertical error that is likely to be present in this reported position. Required when `extrapolated` field is true and always in the entry for the current state.""" + + extrapolated: Optional[bool] = False + """True if this position was generated primarily by computation rather than primarily from a direct instrument measurement. Assumed false if not specified.""" + + pressure_altitude: Optional[float] = -1000 + """The uncorrected altitude (based on reference standard 29.92 inHg, 1013.25 mb) provides a reference for algorithms that utilize "altitude deltas" between aircraft. This value is provided in meters and must have a minimum resolution of 1 meter. Invalid, No Value or Unknown is -1000 m.""" + + height: Optional[RIDHeight] + + +class OperatingArea(ImplicitDict): + """Area of operation containing one or more aircraft participating in remote identification.""" + + aircraft_count: Optional[int] + """Allows for operating a single UA, group, formation, or swarm. Quantity in Group.""" + + volumes: Optional[List[Volume4D]] = [] + """The area where a group or Intent-Based Network Participant operation is planned or taking place.""" + + +class SubscriberToNotify(ImplicitDict): + """Subscriber to notify of a creation/change/deletion of a change in the airspace. This is provided by the DSS to a client changing the airspace, and it is the responsibility of the client changing the airspace (they will receive a set of these notification requests) to send a notification to each specified `url`.""" + + subscriptions: List[SubscriptionState] + """Subscription(s) prompting this notification.""" + + url: URL + """The endpoint that the client mutating the airspace should provide the update to. API depends on the DSS action taken that triggered this notification request.""" + + +class RIDRecentAircraftPosition(ImplicitDict): + time: Time + """Time at which this position applied.""" + + position: RIDAircraftPosition + + +class GetIdentificationServiceAreaDetailsResponse(ImplicitDict): + """Response to request for the details of an identification service area with the given ID.""" + + extents: Volume4D + """The extents of the Identification Service Area.""" + + +class CreateIdentificationServiceAreaParameters(ImplicitDict): + """Parameters for a request to create an Identification Service Area in the DSS.""" + + extents: Volume4D + """The bounding spacetime extents of this Identification Service Area. End time must be specified. If start time is not specified, it will be set to the current time. Start times in the past should be rejected by the DSS, except that it may adjust very recent start times to the current time. + + These extents should not reveal any sensitive information about the flight or flights within them. This means, for instance, that extents should not tightly-wrap a flight path, nor should they generally be centered around the takeoff point of a single flight. + """ + + uss_base_url: FlightsUSSBaseURL + + +class UpdateIdentificationServiceAreaParameters(ImplicitDict): + """Parameters for a request to update an Identification Service Area in the DSS.""" + + extents: Volume4D + """The bounding spacetime extents of this Identification Service Area. End time must be specified. If start time is not specified, it will remain unchanged. Start times in the past should be rejected by the DSS unless they are unchanged from the Identification Service Area's current start time. + + These extents should not reveal any sensitive information about the flight or flights within them. This means, for instance, that extents should not tightly-wrap a flight path, nor should they generally be centered around the takeoff point of a single flight. + """ + + uss_base_url: FlightsUSSBaseURL + + +class CreateSubscriptionParameters(ImplicitDict): + """Parameters for a request to create a subscription in the DSS.""" + + extents: Volume4D + """The spacetime extents of the volume to subscribe to. + + This subscription will automatically be deleted after its end time if it has not been refreshed by then. If end time is not specified, the value will be chosen automatically by the DSS. + + Note that some Entities triggering notifications may lie entirely outside the requested area. + """ + + uss_base_url: SubscriptionUSSBaseURL + + +class UpdateSubscriptionParameters(ImplicitDict): + """Parameters for a request to update a subscription in the DSS.""" + + extents: Volume4D + """The spacetime extents of the volume to subscribe to. + + This subscription will automatically be deleted after its end time if it has not been refreshed by then. If end time is not specified, the value will be chosen automatically by the DSS. + + Note that some Entities triggering notifications may lie entirely outside the requested area. + """ + + uss_base_url: SubscriptionUSSBaseURL + + +class Subscription(ImplicitDict): + """Specification of a geographic area that a client is interested in on an ongoing basis (e.g., "planning area"). Internal to the DSS.""" + + id: SubscriptionUUID + """Unique identifier for this subscription.""" + + uss_base_url: SubscriptionUSSBaseURL + + owner: str + """Assigned by the DSS based on creating client’s ID (via access token). Used for restricting mutation and deletion operations to owner.""" + + notification_index: Optional[SubscriptionNotificationIndex] = 0 + + time_end: Optional[Time] + """If set, this subscription will be automatically removed after this time.""" + + time_start: Optional[Time] + """If set, this Subscription will not generate any notifications before this time.""" + + version: Version + + +class IdentificationServiceArea(ImplicitDict): + """An Identification Service Area (area in which remote ID services are being provided). The DSS reports only these declarations and clients must exchange flight information peer-to-peer.""" + + uss_base_url: FlightsUSSBaseURL + + owner: str + """Assigned by the DSS based on creating client’s ID (via access token). Used for restricting mutation and deletion operations to owner.""" + + time_start: Time + """Beginning time of service.""" + + time_end: Time + """End time of service.""" + + version: Version + + id: EntityUUID + """Unique identifier for this Identification Service Area.""" + + +class RIDAircraftState(ImplicitDict): + """State of an aircraft for the purposes of remote ID.""" + + timestamp: Time + """Time at which this state was valid. This may be the time coming from the source, such as a GPS, or the time when the system computes the values using an algorithm such as an Extended Kalman Filter (EKF). Timestamp must be expressed with a minimum resolution of 1/10th of a second.""" + + timestamp_accuracy: float = 0 + """Declaration of timestamp accuracy, which is the largest difference between Timestamp and true time of applicability for any of the following fields: Latitude, Longitude, Geodetic Altitude, Pressure Altitude of Position, Height. to determine time of applicability of the location data provided. Expressed in seconds, precise to 1/10ths of seconds. The accuracy reflects the 95% uncertainty bound value for the timestamp.""" + + operational_status: Optional[RIDOperationalStatus] + + position: RIDAircraftPosition + + track: Optional[float] = 361 + """Direction of flight expressed as a "True North-based" ground track angle. This value is provided in clockwise degrees with a minimum resolution of 1 degree. If aircraft is not moving horizontally, use the "Unknown" value. A value of 361 indicates invalid, no value, or unknown.""" + + speed: Optional[float] = 255 + """Ground speed of flight in meters per second. Invalid, No Value, or Unknown is 255 m/s, if speed is >254.25 m/s then report 254.25 m/s.""" + + speed_accuracy: SpeedAccuracy + """Provides quality/containment on horizontal ground speed.""" + + vertical_speed: Optional[float] = 63 + """Speed up (vertically) WGS84-HAE, m/s. Invalid, No Value, or Unknown is 63 m/s, if speed is >62 m/s then report 62 m/s.""" + + +class GetSubscriptionResponse(ImplicitDict): + """Response to DSS request for the subscription with the given id.""" + + subscription: Subscription + + +class SearchSubscriptionsResponse(ImplicitDict): + """Response to DSS query for subscriptions in a particular area.""" + + subscriptions: Optional[List[Subscription]] = [] + """Subscriptions that overlap the specified area.""" + + +class DeleteSubscriptionResponse(ImplicitDict): + """Response for a successful request to delete an Subscription.""" + + subscription: Subscription + """The Subscription which was deleted.""" + + +class RIDFlight(ImplicitDict): + """Description of a remote ID flight.""" + + id: RIDFlightID + + aircraft_type: UAType + """Generic type of aircraft.""" + + current_state: Optional[RIDAircraftState] + """The most up-to-date state of the aircraft. Required when the aircraft is + currently in the requested area unless no telemetry is expected during during + the flight and `volumes` is specified. + + If current data is not being received from the UAS by the Service Provider, + the lack of change in this field is sufficient to indicate that current + data is not being received. + + If the USS believes a flight is airborne and expects to receive telemetry + at some point during the flight but has not yet received telemetry, this + field should be populated with the USS's best estimate of the position + of the flight with appropriate accuracy indications. + """ + + operating_area: Optional[OperatingArea] + """The area the aircraft is/are within. Required, with 1 or more elements, if + `current_state` is not specified. The fields `time_start` and `time_end` are + required. + + If `current_state` is specified, this field should not be specified. + If `current_state` was available at any time during the flight, or is + expected to be available at any time during the flight, this field should + not be specified. + """ + + simulated: Optional[bool] = False + """If specified as true, this flight is not a physical aircraft; it is just a simulation to test the system.""" + + recent_positions: Optional[List[RIDRecentAircraftPosition]] = [] + """A short collection of recent aircraft movement, specified only when `recent_positions_duration` is greater than zero. If `volumes` is not specified and `recent_positions_duration` is greater than zero, then this field is required. + + Recent positions provided in this field must conform to requirements in the standard which generally prohibit including positions outside the requested area except transitionally when the aircraft enters or exits the requested area, and which prohibit including positions that not sufficiently recent. + + Note that a UI should not draw a connective line between two consecutive position reports that both lie outside the requested area. + """ + + +class PutIdentificationServiceAreaResponse(ImplicitDict): + """Response to a request to create or update a reference to an Identification Service Area in the DSS.""" + + subscribers: Optional[List[SubscriberToNotify]] = [] + """DSS subscribers that this client now has the obligation to notify of the Identification Service Area changes just made. This client must call POST for each provided URL according to the `/uss/identification_service_areas/{id}` path API.""" + + service_area: IdentificationServiceArea + """Resulting service area stored in DSS.""" + + +class SearchIdentificationServiceAreasResponse(ImplicitDict): + """Response to DSS query for Identification Service Areas in an area of interest.""" + + service_areas: Optional[List[IdentificationServiceArea]] = [] + """Identification Service Areas in the area of interest.""" + + +class PutIdentificationServiceAreaNotificationParameters(ImplicitDict): + """Parameters of a message informing of new full information for an Identification Service Area. Pushed (by a client, not the DSS) directly to clients with subscriptions when another client makes a change to airspace within a cell with a subscription.""" + + service_area: Optional[IdentificationServiceArea] + """Identification Service Area that the notifying client changed or created. + + If this field is populated, the Identification Service Area was created or updated. If this field is not populated, the Identification Service Area was deleted. + """ + + subscriptions: List[SubscriptionState] + """Subscription(s) prompting this notification.""" + + extents: Optional[Volume4D] + """The new or updated extents of the Identification Service Area. + + Omitted if Identification Service Area was deleted. + """ + + +class DeleteIdentificationServiceAreaResponse(ImplicitDict): + """Response for a request to delete an Identification Service Area.""" + + service_area: IdentificationServiceArea + """Identification Service Area that was just deleted.""" + + subscribers: Optional[List[SubscriberToNotify]] = [] + """DSS subscribers that this client now has the obligation to notify of the Identification Service Area just deleted. This client must call POST for each provided URL according to the `/uss/identification_service_areas` path API.""" + + +class PutSubscriptionResponse(ImplicitDict): + """Response for a request to create or update a subscription.""" + + service_areas: Optional[List[IdentificationServiceArea]] = [] + """Identification Service Areas in or near the subscription area at the time of creation/update, if `identification_service_area_url` callback was specified.""" + + subscription: Subscription + """Result of the operation on the subscription.""" + + +class GetIdentificationServiceAreaResponse(ImplicitDict): + """Response to DSS request for the identification service area with the given ID.""" + + service_area: IdentificationServiceArea + + +class GetFlightsResponse(ImplicitDict): + """Response to remote ID provider query for flight information in an area of interest.""" + + timestamp: Time + """The remote ID service provider's timestamp for when this information was current.""" + + flights: Optional[List[RIDFlight]] = [] + """A list of all flights that have been within the requested area within the remote ID retention period. This includes flights that are not currently within the requested area, but were within the requested area within the remote ID retention period. Aircraft that are not in flight, and have not been in flight within the retention period, should not be included in this list.""" + + no_isas_present: Optional[bool] = False + """The requested view is entirely outside any remote ID service provision areas (Identification Service Area volumes) this provider has. A display provider receiving this response should discontinue polling for this endpoint for the view port requested until otherwise directed by DSS interactions.""" diff --git a/src/uas_standards/astm/f3548/__init__.py b/src/uas_standards/astm/f3548/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/astm/f3548/v21/__init__.py b/src/uas_standards/astm/f3548/v21/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/astm/f3548/v21/api.py b/src/uas_standards/astm/f3548/v21/api.py new file mode 100644 index 0000000..98e0e33 --- /dev/null +++ b/src/uas_standards/astm/f3548/v21/api.py @@ -0,0 +1,1063 @@ +"""Data types from UTM API (USS->DSS and USS->USS) 1.0.0 OpenAPI""" + +# This file is autogenerated; do not modify manually! + +from __future__ import annotations + +from enum import Enum +from typing import List, Optional + +from implicitdict import ImplicitDict, StringBasedDateTime + + +UUIDv4Format = str +"""String whose format matches a version-4 UUID according to RFC 4122.""" + + +EntityID = UUIDv4Format + + +EntityOVN = str +"""A token associated with a particular UTM Entity+version created by the DSS upon creation or modification of an Entity reference and provided to the client creating or modifying the Entity reference. The EntityOVN is stored privately by the DSS and then compared against entries in a Key provided to mutate the airspace. The EntityOVN is also provided by the client whenever that client transmits the full information of the Entity (either via GET, or via a subscription notification).""" + + +SubscriptionID = UUIDv4Format + + +Key = List[EntityOVN] + + +class TimeFormat(str, Enum): + RFC3339 = "RFC3339" + + +class Time(ImplicitDict): + value: StringBasedDateTime + """RFC3339-formatted time/date string. The time zone must be 'Z'.""" + + format: TimeFormat = TimeFormat.RFC3339 + + +class RadiusUnits(str, Enum): + """FIXM-compatible units. Only meters ("M") are acceptable for UTM.""" + + M = "M" + + +class Radius(ImplicitDict): + value: float + """Distance from the centerpoint of a circular area, along the WGS84 ellipsoid.""" + + units: RadiusUnits = RadiusUnits.M + """FIXM-compatible units. Only meters ("M") are acceptable for UTM.""" + + +class AltitudeReference(str, Enum): + """A code indicating the reference for a vertical distance. See AIXM 5.1 and FIXM 4.2.0. Currently, UTM only allows WGS84 with no immediate plans to allow other options. FIXM and AIXM allow for 'SFC' which is equivalent to AGL.""" + + W84 = "W84" + + +class AltitudeUnits(str, Enum): + """The reference quantities used to express the value of altitude. See FIXM 4.2. Currently, UTM only allows meters with no immediate plans to allow other options.""" + + M = "M" + + +class Altitude(ImplicitDict): + value: float + """The numeric value of the altitude. Note that min and max values are added as a sanity check. As use cases evolve and more options are made available in terms of units of measure or reference systems, these bounds may be re-evaluated.""" + + reference: AltitudeReference = AltitudeReference.W84 + """A code indicating the reference for a vertical distance. See AIXM 5.1 and FIXM 4.2.0. Currently, UTM only allows WGS84 with no immediate plans to allow other options. FIXM and AIXM allow for 'SFC' which is equivalent to AGL.""" + + units: AltitudeUnits = AltitudeUnits.M + """The reference quantities used to express the value of altitude. See FIXM 4.2. Currently, UTM only allows meters with no immediate plans to allow other options.""" + + +Latitude = float +"""Degrees of latitude north of the equator, with reference to the WGS84 ellipsoid.""" + + +Longitude = float +"""Degrees of longitude east of the Prime Meridian, with reference to the WGS84 ellipsoid.""" + + +class LatLngPoint(ImplicitDict): + """Point on the earth's surface.""" + + lng: Longitude + + lat: Latitude + + +class Circle(ImplicitDict): + """A circular area on the surface of the earth.""" + + center: Optional[LatLngPoint] + + radius: Optional[Radius] + + +class ErrorResponse(ImplicitDict): + """Human-readable string returned when an error occurs as a result of a USS - DSS transaction.""" + + message: Optional[str] + """Human-readable message indicating what error occurred and/or why.""" + + +SubscriptionNotificationIndex = int +"""Tracks the notifications sent for a subscription so the subscriber can detect missed notifications more easily.""" + + +UssBaseURL = str +"""The base URL of a USS implementation of part or all of the USS-USS API. Per the USS-USS API, the full URL of a particular resource can be constructed by appending, e.g., `/uss/v1/{resource}/{id}` to this base URL. Accordingly, this URL may not have a trailing '/' character.""" + + +class OperationalIntentState(str, Enum): + """State of an operational intent. + 'Accepted': Operational intent is created and shared, but not yet in use; see standard text for more details. + The create or update request for this operational intent reference must include a Key containing all OVNs for all relevant Entities. + 'Activated': Operational intent is in active use; see standard text for more details. + The create or update request for this operational intent reference must include a Key containing all OVNs for all relevant Entities. + 'Nonconforming': UA is temporarily outside its volumes, but the situation is expected to be recoverable; see standard text for more details. + In this state, the `/uss/v1/operational_intents/{entityid}/telemetry` USS-USS endpoint should respond, if available, to queries from USS peers. The create or update request for this operational intent may omit a Key in this case because the operational intent is being adjusted as flown and cannot necessarily deconflict. + 'Contingent': UA is considered unrecoverably unable to conform with its coordinate operational intent; see standard text for more details. + This state must transition to Ended. In this state, the `/uss/v1/operational_intents/{entityid}/telemetry` USS-USS endpoint should respond, if available, to queries from USS peers. The create or update request for this operational intent may omit a Key in this case because the operational intent is being adjusted as flown and cannot necessarily deconflict. + """ + + Accepted = "Accepted" + Activated = "Activated" + Nonconforming = "Nonconforming" + Contingent = "Contingent" + + +OperationalIntentUssBaseURL = UssBaseURL + + +ConstraintUssBaseURL = UssBaseURL + + +Priority = int +"""Ordinal priority of the operational intent, as defined by the regulator. Operational intents with lesser values are lower priority than all operational intents with greater values. A lower-priority operational intent may not create a conflict with a higher-priority operational intent. A higher-priority operational intent may create a conflict with a lower-priority operational intent. The regulator specifies whether an operational intent may create a conflict with other operational intents of the same priority.""" + + +class PositionAccuracyVertical(str, Enum): + """Vertical error that is likely to be present in this reported position. This is the GVA enumeration from ADS-B, plus some finer values for UAS.""" + + VAUnknown = "VAUnknown" + VA150mPlus = "VA150mPlus" + VA150m = "VA150m" + VA45m = "VA45m" + VA25m = "VA25m" + VA10m = "VA10m" + VA3m = "VA3m" + VA1m = "VA1m" + + +class PositionAccuracyHorizontal(str, Enum): + """Horizontal error that is likely to be present in this reported position. This is the NACp enumeration from ADS-B, plus 1m for a more complete range for UAS.""" + + HAUnknown = "HAUnknown" + HA10NMPlus = "HA10NMPlus" + HA10NM = "HA10NM" + HA4NM = "HA4NM" + HA2NM = "HA2NM" + HA1NM = "HA1NM" + HA05NM = "HA05NM" + HA03NM = "HA03NM" + HA01NM = "HA01NM" + HA005NM = "HA005NM" + HA30m = "HA30m" + HA10m = "HA10m" + HA3m = "HA3m" + HA1m = "HA1m" + + +class Position(ImplicitDict): + """Location of the vehicle (UAS) as reported for UTM. Note: 'accuracy' values are required when extrapolated field is true.""" + + longitude: Optional[Longitude] + + latitude: Optional[Latitude] + + accuracy_h: Optional[PositionAccuracyHorizontal] + + accuracy_v: Optional[PositionAccuracyVertical] + + extrapolated: Optional[bool] = False + """True if this position was generated primarily by computation rather than primarily from a direct instrument measurement.""" + + altitude: Optional[Altitude] + + +class VelocityUnits_speed(str, Enum): + MetersPerSecond = "MetersPerSecond" + + +class Velocity(ImplicitDict): + speed: float + """Ground speed in meters/second.""" + + units_speed: VelocityUnits_speed = VelocityUnits_speed.MetersPerSecond + + track: Optional[float] = 0 + """Direction of flight expressed as a "True North-based" ground track angle. This value is provided in degrees East of North with a minimum resolution of 1 degree. A value of 360 indicates invalid, no value, or unknown.""" + + +class UssAvailabilityState(str, Enum): + """A USS is presumed to be in the Unknown state absent indication otherwise by a USS with availability arbitration scope. Upon determination via availability arbitration, a USS is Down when it does not respond appropriately, and a Down USS may not perform the following operations in the DSS: + * Create an operational intent in the Accepted or Activated states + * Modify an operational intent whose new or unchanged state is Accepted or Activated + * Delete an operational intent + A USS in the Unknown state possesses all the capabilities, within the DSS, of a USS in the Normal state. + """ + + Unknown = "Unknown" + Normal = "Normal" + Down = "Down" + + +class SetUssAvailabilityStatusParameters(ImplicitDict): + old_version: str = "" + """Version of USS's availability to change, for consistent read-modify-write operations and consistent retry behavior.""" + + availability: UssAvailabilityState + + +class ExchangeRecordRecorder_role(str, Enum): + """A coded value that indicates the role of the logging USS: 'Client' (initiating a request to a remote USS) or 'Server' (handling a request from a remote USS)""" + + Client = "Client" + Server = "Server" + + +class ExchangeRecord(ImplicitDict): + """Details of a request/response data exchange.""" + + url: str + """Full URL of request.""" + + method: str + """HTTP verb used by requestor (e.g., "PUT," "GET," etc.)""" + + headers: Optional[List[str]] = [] + """Set of headers associated with request or response. Requires 'Authorization:' field (at a minimum)""" + + recorder_role: ExchangeRecordRecorder_role + """A coded value that indicates the role of the logging USS: 'Client' (initiating a request to a remote USS) or 'Server' (handling a request from a remote USS)""" + + request_time: Time + """The time at which the request was sent/received.""" + + request_body: Optional[str] = "" + """Base64-encoded body content sent/received as a request.""" + + response_time: Optional[Time] + """The time at which the response was sent/received.""" + + response_body: Optional[str] = "" + """Base64-encoded body content sent/received in response to request.""" + + response_code: Optional[int] = 0 + """HTTP response code sent/received in response to request.""" + + problem: Optional[str] + """'Human-readable description of the problem with the exchange, if any.'""" + + +class ErrorReport(ImplicitDict): + """A report informing a server of a communication problem.""" + + report_id: Optional[str] + """ID assigned by the server receiving the report. Not populated when submitting a report.""" + + exchange: ExchangeRecord + """The request (by this USS) and response associated with the error.""" + + +class PlanningRecord(ImplicitDict): + """A record of a single attempt to (successfully or unsuccessfully) create or modify an operational intent.""" + + time: Time + """Time that this planning event occurred""" + + ovns: List[EntityOVN] = [] + """OVNs the planning USS was aware of when it was planning the operational intent""" + + missing_operational_intents: Optional[List[EntityID]] = [] + """List of missing operational intents (for planning attempts that were denied by the DSS with code 409)""" + + missing_constraints: Optional[List[EntityID]] = [] + """List of missing constraints (for planning attempts that were denied by the DSS with code 409)""" + + operational_intent_id: Optional[EntityID] + """ID of the operational intent being planned""" + + problem: Optional[str] + """A free text description of the problem(s) encountered during this planning attempt.""" + + +class UserNotificationRecordNotification_triggering_event(str, Enum): + """Requirement ID that pertains to the given notification""" + + GEN0400 = "GEN0400" + GEN0405 = "GEN0405" + SCD0090 = "SCD0090" + SCD0095 = "SCD0095" + ACM0010 = "ACM0010" + CMSA0115 = "CMSA0115" + CMSA0300 = "CMSA0300" + CSTP0005 = "CSTP0005" + CSTP0010 = "CSTP0010" + CSTP0020 = "CSTP0020" + CSTP0025 = "CSTP0025" + CSTP0030 = "CSTP0030" + CSTO0035 = "CSTO0035" + + +class UserNotificationRecord(ImplicitDict): + """User notification record.""" + + triggering_event_time: Time + """Time of the notification triggering event""" + + notification_time: Time + """Time at which the user was notified""" + + notification_details: Optional[str] + """Description of information that was provided to the user, as per the referenced notification_triggering_event requirement""" + + notification_triggering_event: UserNotificationRecordNotification_triggering_event + """Requirement ID that pertains to the given notification""" + + +class UserInputRecordInput_triggering_event(str, Enum): + """Requirement ID that pertains to the given notification""" + + OPIN0040 = "OPIN0040" + CMSA0010 = "CMSA0010" + CMSA0025 = "CMSA0025" + CMSA0100 = "CMSA0100" + CMSA0105 = "CMSA0105" + CMSA0110 = "CMSA0110" + CMSA0200 = "CMSA0200" + CMSA0205 = "CMSA0205" + CMSA0210 = "CMSA0210" + CMSA0215 = "CMSA0215" + + +class UserInputRecord(ImplicitDict): + """User input record""" + + triggering_event_time: Time + """Time in which user input was received by the USS""" + + operational_intent_id: EntityID + """ID of the operational_intent ID pertaining to the user input""" + + input_triggering_event: UserInputRecordInput_triggering_event + """Requirement ID that pertains to the given notification""" + + input_details: Optional[str] + """Description of the information that was provided by the user, as per the referenced input_triggering_event requirement""" + + +class OperatorAssociation(ImplicitDict): + """Association between an operational intent and the operator of that operational intent""" + + operational_intent_id: EntityID + """ID of operational intent to which this association pertains""" + + operator_id: str + """Unique identifier of the operator responsible for the operational intent""" + + +class ConstraintProviderAssociation(ImplicitDict): + """Association between a constraint and the constraint provider responsible for that constraint""" + + constraint_id: EntityID + """ID of constraint to which this association pertains""" + + constraint_provider_id: str + """Unique identifier of the constraint provider responsible for the constraint""" + + +class GeoZoneAdditional_properties(ImplicitDict): + """Indicates that exemptions from the national or European regulations are allowed in the UAS Zone, that will be detailed via the "message" property.""" + + + +CodeZoneIdentifierType = str +"""a string of maximum 7 characters that uniquely identifies the area within a geographical scope. +NOTE (1): This shall not include the country identifier, which is a separate attribute of the UAS Zone. +NOTE (2): The length of this data type is limited to 7 characters for compatibility with ARINC 424 and AIXM, where an airspace designator may have maximum 10 characters. The 10 characters are the result of concatenating the UAS Zone attributes for country and identifier. +""" + + +CodeCountryISOType = str +"""A 3 letter identifier of a country or territory using the ISO 3166-1 alpha-3 standard. +NOTE: >- + The ISO 3-letter country codes come with the following advantages: + - allow to distinguish between remote territories and mainland + - are unique, unlike the ICAO Country codes where the same State + could have two or more codes + - are also used in military standards, such as NATO STANAG 1059 + INT, which come with well document additions that might be also + useful for UAS areas. +""" + + +class CodeZoneType(str, Enum): + """A coded list of values which allows indicating that the definition of a UAS Zone is specifically customised for a particular UAS or operator.""" + + COMMON = "COMMON" + CUSTOMIZED = "CUSTOMIZED" + PROHIBITED = "PROHIBITED" + REQ_AUTHORISATION = "REQ_AUTHORISATION" + CONDITIONAL = "CONDITIONAL" + NO_RESTRICTION = "NO_RESTRICTION" + + +ConditionExpressionType = str +"""A coded expression that provides information about what is authorised / forbidden in a zone that has conditional access. +By difference with the “Message” field per zone, this coded expression is made to be interopreted by the UAS while the “Message” is to interpreted by the remote pilot. +NOTE: the maximum field length is 10 000 characters. +---------------------- Condition definition language ---------------- • A list of relevant characteristics (CHARTYPE) has first to be established per state, and their finite list of acceptable values (CHARVAL) +• Each chartype and charval fields are defined by a limited set of characters +• A public document shall give the definitions of each, and provide the reference to legal or technical characteristics implied +• The Geozone editor per state can use these characteristics, with the dedicated condition language defined below, to define exact conditions per zone +• Each UAS Geofencing function shall be loaded with the corresponding chararacteristic status of the UAS for the intended flight, so as to be able to apply the conditions , either to generate alerts or to limit the flight +• If the value of a given characteristic of the condition equation is not defined in the UAS, the UAS Geofencing function should inform the pilot in Geoawareness alerting or consider that the zone is forbidden, by default in automatic Geofencing. +A conditional expression shall be of the following type: +• The UAS is PERMITTED XOR PROHIBITED (exclusive choice) to fly in this zone at this time IF (Characteristic1) CHARTYPE1 = (Value1) CHARVAL1 AND +CHARTYPE 2 = CHARVAL 2 AND ... AND End IF +OR (...) +... +End OR +• Only the fields in bold need to be edited in the character string, separated by”/”. Others are implicit. +Examples of CHARTYPE and CHARVALUE: +• CHARTYPE: operator type; Acceptable CHARVAL values: Military/Police/Firefighting +• CHARTYPE: Operator ID (registration number); Acceptable CHARVAL values: as per registration format +• CHARTYPE: Operation type: A1 as per EASA Open Types or S1 (National standard Scenario 1), STS01 (EASA Specific standard scenario) or ... +• CHARTYPE: UTM operation type: Planned/Unplanned, +• CHARTYPE: passengers on board: yes /no Note that it is possible in each national catalog of chartype and charval items, to define complex categories of operation/drone /equipment. Example: In nation A, we may have a type “drone level” with values Low, Medium, High. Each level corresponds to a defined set of required UAS performance/operation features/ operator qualification etc. This avoids to code a complex combination in the geozone database. This conditional expression can also be used to code a prohibition of image capture in a zone. +Example: PERMITTED/IMAGE CAPTURE=NO/NOISE + + CLASS=A/OR/OPERATOR=POLICE + +Meaning: >- + the fight is permitted in this zone at that time if No image is +captured (removed or deactivated) and if noise class = class A (following a known classification) or if the UAS operator is the Police +""" + + +CodeRestrictionType = str +"""An indication if flying in the zone is conditional, forbidden or unrestricted.""" + + +class CodeZoneReasonType(str, Enum): + """A coded indication of a reason that justifies the existence of an UAS Zone""" + + AIR_TRAFFIC = "AIR_TRAFFIC" + SENSITIVE = "SENSITIVE" + PRIVACY = "PRIVACY" + POPULATION = "POPULATION" + NATURE = "NATURE" + NOISE = "NOISE" + FOREIGN_TERRITORY = "FOREIGN_TERRITORY" + EMERGENCY = "EMERGENCY" + OTHER = "OTHER" + + +CodeUSpaceClassType = str +"""A coded identifier for a category or class of the zone applying a "USpace concept". +NOTE: >- + In the current model version, there is no specific list of values. +For example, the “X”, “Y”, “Z” types of zones as per SESAR JU Corus project on USpace concept of operation could be used in a future version. Until a precise list of values is defined, this data type will be considered as string of characters of maximum 100 characters. +""" + + +CodeYesNoType = str +"""A coded value that indicates a choice between a positive (yes) or a negative (no) applicability. + +Acceptable values: +* True +* False +""" + + +class CodeAuthorityRole(str, Enum): + """A coded list of values indicating the role that an authority has in relation with the UAS zone.""" + + AUTHORIZATION = "AUTHORIZATION" + NOTIFICATION = "NOTIFICATION" + INFORMATION = "INFORMATION" + + +TextShortType = str +"""A free text with a maximum length of 200 characters""" + + +class Polygon(ImplicitDict): + """An enclosed area on the earth. The bounding edges of this polygon are defined to be the shortest paths between connected vertices. This means, for instance, that the edge between two points both defined at a particular latitude is not generally contained at that latitude. The winding order must be interpreted as the order which produces the smaller area. The path between two vertices is defined to be the shortest possible path between those vertices. Edges may not cross. Vertices may not be duplicated. In particular, the final polygon vertex must not be identical to the first vertex.""" + + vertices: List[LatLngPoint] + + +class Volume3D(ImplicitDict): + """A three-dimensional geographic volume consisting of a vertically-extruded shape. Exactly one outline must be specified.""" + + outline_circle: Optional[Circle] + """A circular geographic shape on the surface of the earth.""" + + outline_polygon: Optional[Polygon] + """A polygonal geographic shape on the surface of the earth.""" + + altitude_lower: Optional[Altitude] + """Minimum bounding altitude of this volume. Must be less than altitude_upper, if specified.""" + + altitude_upper: Optional[Altitude] + """Maximum bounding altitude of this volume. Must be greater than altitude_lower, if specified.""" + + +class Volume4D(ImplicitDict): + """Contiguous block of geographic spacetime.""" + + volume: Volume3D + + time_start: Optional[Time] + """Beginning time of this volume. Must be before time_end.""" + + time_end: Optional[Time] + """End time of this volume. Must be after time_start.""" + + +class SubscriptionState(ImplicitDict): + """State of subscription which is causing a notification to be sent.""" + + subscription_id: SubscriptionID + + notification_index: SubscriptionNotificationIndex + + +class QuerySubscriptionParameters(ImplicitDict): + """Parameters for a request to find subscriptions matching the provided criteria.""" + + area_of_interest: Optional[Volume4D] + + +SubscriptionUssBaseURL = UssBaseURL + + +class OperationalIntentReference(ImplicitDict): + """The high-level information of a planned or active operational intent with the URL of a USS to query for details. Note: 'ovn' is returned ONLY to the USS that created the operational intent but NEVER to other USS instances.""" + + id: EntityID + + manager: str + """Created by the DSS based on creating client's ID (via access token). Used internal to the DSS for restricting mutation and deletion operations to manager. Used by USSs to reject operational intent update notifications originating from a USS that does not manage the operational intent.""" + + uss_availability: UssAvailabilityState + + version: int + """Numeric version of this operational intent which increments upon each change in the operational intent, regardless of whether any field of the operational intent reference changes. A USS with the details of this operational intent when it was at a particular version does not need to retrieve the details again until the version changes.""" + + state: OperationalIntentState + + ovn: Optional[EntityOVN] + """Opaque version number of this operational intent. Populated only when the OperationalIntentReference is managed by the USS retrieving or providing it. Not populated when the OperationalIntentReference is not managed by the USS retrieving or providing it (instead, the USS must obtain the OVN from the details retrieved from the managing USS).""" + + time_start: Time + """Beginning time of operational intent.""" + + time_end: Time + """End time of operational intent.""" + + uss_base_url: OperationalIntentUssBaseURL + + subscription_id: SubscriptionID + """The ID of the subscription that is ensuring the operational intent manager receives relevant airspace updates.""" + + +class ImplicitSubscriptionParameters(ImplicitDict): + """Information necessary to create a subscription to serve a single operational intent's notification needs.""" + + uss_base_url: SubscriptionUssBaseURL + """The base URL of a USS implementation of the parts of the USS-USS API necessary for receiving the notifications that the operational intent must be aware of. This includes, at least, notifications for relevant changes in operational intents.""" + + notify_for_constraints: Optional[bool] = False + """True if this operational intent's subscription should trigger notifications when constraints change. Otherwise, changes in constraints should not trigger notifications. The scope utm.constraint_processing is required to set this flag true, and a USS performing the constraint processing role should set this flag true.""" + + +class GetOperationalIntentReferenceResponse(ImplicitDict): + """Response to DSS request for the OperationalIntentReference with the given ID.""" + + operational_intent_reference: OperationalIntentReference + + +class QueryOperationalIntentReferenceParameters(ImplicitDict): + """Parameters for a request to find OperationalIntentReferences matching the provided criteria.""" + + area_of_interest: Optional[Volume4D] + + +class QueryOperationalIntentReferenceResponse(ImplicitDict): + """Response to DSS query for OperationalIntentReferences in an area of interest.""" + + operational_intent_references: List[OperationalIntentReference] = [] + """OperationalIntentReferences in the area of interest.""" + + +class ConstraintReference(ImplicitDict): + """A ConstraintReference (area in which a constraint is present, along with other high-level information, but no details). The DSS reports only these references and clients must exchange details and additional information peer-to-peer.""" + + id: EntityID + + manager: str + """Created by the DSS based on creating client's ID (via access token). Used internal to the DSS for restricting mutation and deletion operations to manager. Used by USSs to reject constraint update notifications originating from a USS that does not manage the constraint.""" + + uss_availability: UssAvailabilityState + + version: int + """Numeric version of this constraint which increments upon each change in the constraint, regardless of whether any field of the constraint reference changes. A USS with the details of this constraint when it was at a particular version does not need to retrieve the details again until the version changes.""" + + ovn: Optional[EntityOVN] + """Opaque version number of this constraint. Populated only when the ConstraintReference is managed by the USS retrieving or providing it. Not populated when the ConstraintReference is not managed by the USS retrieving or providing it (instead, the USS must obtain the OVN from the details retrieved from the managing USS).""" + + time_start: Time + + time_end: Time + + uss_base_url: ConstraintUssBaseURL + + +class PutConstraintReferenceParameters(ImplicitDict): + """Parameters for a request to create/update a ConstraintReference in the DSS.""" + + extents: List[Volume4D] + """Spacetime extents that bound this constraint. + The end time may not be in the past. + All volumes of the constraint must be encompassed in these extents. However, these extents do not need to match the precise volumes of the constraint; a single bounding extent may be provided instead, for instance. + """ + + uss_base_url: ConstraintUssBaseURL + + +class GetConstraintReferenceResponse(ImplicitDict): + """Response to DSS request for the ConstraintReference with the given ID.""" + + constraint_reference: ConstraintReference + + +class QueryConstraintReferenceParameters(ImplicitDict): + """Parameters for a request to find ConstraintReferences matching the provided criteria.""" + + area_of_interest: Optional[Volume4D] + + +class QueryConstraintReferencesResponse(ImplicitDict): + """Response to DSS query for ConstraintReferences in an area of interest.""" + + constraint_references: List[ConstraintReference] = [] + """ConstraintReferences in the area of interest.""" + + +class AirspaceConflictResponse(ImplicitDict): + """Data provided when an airspace conflict was encountered.""" + + message: Optional[str] + """Human-readable message indicating what error occurred and/or why.""" + + missing_operational_intents: Optional[List[OperationalIntentReference]] = [] + """List of operational intent references for which current proof of knowledge was not provided. If this field is present and contains elements, the calling USS should query the details URLs for these operational intents to obtain their details and correct OVNs. The OVNs can be used to update the key, at which point the USS may retry this call.""" + + missing_constraints: Optional[List[ConstraintReference]] = [] + """List of constraint references for which current proof of knowledge was not provided. If this field is present and contains elements, the calling USS should query the details URLs for these constraints to obtain their details and correct OVNs. The OVNs can be used to update the key, at which point the USS may retry this call.""" + + +class OperationalIntentDetails(ImplicitDict): + """Details of a UTM operational intent. Note that this data is not stored in the DSS; only with the clients.""" + + volumes: Optional[List[Volume4D]] = [] + """Volumes that wholly contain the operational intent while being as small as practical. + Start and end times, as well as lower and upper altitudes, are required for each volume. The end time may not be in the past. + Required with at least one item when the operational intent is Accepted, Activated, or Nonconforming. + May not contain any items when the operational intent is Contingent. + """ + + off_nominal_volumes: Optional[List[Volume4D]] = [] + """Volumes that contain the anticipated area of non-conformance while the aircraft is in the Nonconforming or Contingent states. + Start and end times, as well as lower and upper altitudes, are required for each volume. The end time may not be in the past. + Required with at least one item when the operational intent is Nonconforming or Contingent. + May not contain any items when the operational intent is Accepted or Activated. + """ + + priority: Optional[Priority] + + +class OperationalIntent(ImplicitDict): + """Full description of a UTM operational intent.""" + + reference: OperationalIntentReference + + details: OperationalIntentDetails + + +class PutOperationalIntentDetailsParameters(ImplicitDict): + """Parameters of a message informing of detailed information for a peer operational intent. Pushed (by a client, not the DSS) directly to clients with subscriptions when another client makes a change to airspace within a cell with a subscription.""" + + operational_intent_id: EntityID + """ID of operational intent that has changed.""" + + operational_intent: Optional[OperationalIntent] + """Full information about the operational intent that has changed. If this field is omitted, the operational intent was deleted. The `ovn` field in the nested `reference` must be populated.""" + + subscriptions: List[SubscriptionState] + """Subscription(s) prompting this notification.""" + + +class GetOperationalIntentDetailsResponse(ImplicitDict): + """Response to peer request for the details of operational intent with the given ID.""" + + operational_intent: OperationalIntent + + +class VehicleTelemetry(ImplicitDict): + """Vehicle position, altitude, and velocity.""" + + time_measured: Time + + position: Optional[Position] + + velocity: Optional[Velocity] + + +class UssAvailabilityStatus(ImplicitDict): + uss: str + """Client ID (matching their `sub` in access tokens) of the USS to which this availability applies.""" + + availability: UssAvailabilityState + + +class UssAvailabilityStatusResponse(ImplicitDict): + version: str + """Current version of USS's availability. Used to change USS's availability.""" + + status: UssAvailabilityStatus + + +class PositionRecord(ImplicitDict): + """A record of vehicle telemetry information received by this USS (typically for conformance monitoring).""" + + time_received: Time + """Time that this position data was received by the USS""" + + telemetry: VehicleTelemetry + + +class OperationalIntentPositions(ImplicitDict): + """A record of position data gathered through the course of an operational intent""" + + positions: Optional[List[PositionRecord]] = [] + + operational_intent_id: EntityID + """ID of the operational intent associated with `positions`""" + + +class Authority(ImplicitDict): + """A relevant authority that is in charge for authorising, being notified or providing information for UAS operations in the UAS zone. + Rule: >- + at least one of the following shall be specified - siteURL, email, + phone. + """ + + name: Optional[TextShortType] + """The official name of a public or private authority""" + + service: Optional[TextShortType] + """The name of a specific department or service within the organisation""" + + contact_name: Optional[TextShortType] + """The name or role of a specific person that needs to be contacted within the organisation""" + + site_url: Optional[TextShortType] + """The URL of the public internet site through which the organisation may be contacted + Note: in the data coding format, this might be further constrained in order to ensure a valid URL format. + """ + + email: Optional[TextShortType] + """The e-mail address by which the organisation may be contacted. + Note: in the data coding format, this might be further constrained in order to ensure a valid e-mail format. + """ + + phone: Optional[TextShortType] + """A phone number at which the organisation may be contacted""" + + purpose: Optional[CodeAuthorityRole] + """The role of the Authority in relation with the zone.""" + + interval_before: Optional[str] + """The minimal time interval required between notification or authorization request and starting to operate in the zone, in the format PnnDTnnHnnM (ISO 8601).""" + + +class SubscriberToNotify(ImplicitDict): + """Subscriber to notify of a change in the airspace. This is provided by the DSS to a client changing the airspace, and it is the responsibility of that client to send a notification to the specified USS according to the change made to the airspace.""" + + subscriptions: List[SubscriptionState] + """Subscription(s) prompting this notification.""" + + uss_base_url: SubscriptionUssBaseURL + + +class Subscription(ImplicitDict): + """Specification of a geographic area that a client is interested in on an ongoing basis (e.g., "planning area").""" + + id: SubscriptionID + + version: str + """Version of the subscription that the DSS changes every time a USS changes the subscription. The DSS incrementing the notification_index does not constitute a change that triggers a new version. A USS must specify this version when modifying an existing subscription to ensure consistency in read-modify-write operations and distributed systems.""" + + notification_index: SubscriptionNotificationIndex + + time_start: Optional[Time] + """If set, this subscription will not receive notifications involving airspace changes entirely before this time.""" + + time_end: Optional[Time] + """If set, this subscription will not receive notifications involving airspace changes entirely after this time.""" + + uss_base_url: SubscriptionUssBaseURL + + notify_for_operational_intents: Optional[bool] = False + """If true, trigger notifications when operational intents are created, updated, or deleted. Otherwise, changes in operational intents should not trigger notifications. The scope utm.strategic_coordination is required to set this flag true.""" + + notify_for_constraints: Optional[bool] = False + """If true, trigger notifications when constraints are created, updated, or deleted. Otherwise, changes in constraints should not trigger notifications. The scope utm.constraint_processing is required to set this flag true.""" + + implicit_subscription: Optional[bool] = False + """True if this subscription was implicitly created by the DSS via the creation of an operational intent, and should therefore be deleted by the DSS when that operational intent is deleted.""" + + dependent_operational_intents: Optional[List[EntityID]] = [] + """List of IDs for operational intents that are dependent on this subscription.""" + + +class QuerySubscriptionsResponse(ImplicitDict): + """Response to DSS query for subscriptions in a particular geographic area.""" + + subscriptions: List[Subscription] = [] + """Subscriptions that overlap the specified geographic area.""" + + +class GetSubscriptionResponse(ImplicitDict): + """Response to DSS request for the subscription with the given id.""" + + subscription: Subscription + + +class PutSubscriptionParameters(ImplicitDict): + """Parameters for a request to create/update a subscription in the DSS. At least one form of notifications must be requested.""" + + extents: Volume4D + """Spacetime extents of the volume to subscribe to. + This subscription will automatically be deleted after its end time if it has not been refreshed by then. If end time is not specified, the value will be chosen automatically by the DSS. If start time is not specified, it will default to the time the request is processed. The end time may not be in the past. + Note that some Entities triggering notifications may lie entirely outside the requested area. + """ + + uss_base_url: SubscriptionUssBaseURL + + notify_for_operational_intents: Optional[bool] = False + """If true, trigger notifications when operational intents are created, updated, or deleted. Otherwise, changes in operational intents should not trigger notifications. The scope utm.strategic_coordination is required to set this flag true.""" + + notify_for_constraints: Optional[bool] = False + """If true, trigger notifications when constraints are created, updated, or deleted. Otherwise, changes in constraints should not trigger notifications. The scope utm.constraint_processing is required to set this flag true.""" + + +class PutSubscriptionResponse(ImplicitDict): + """Response for a request to create or update a subscription.""" + + subscription: Subscription + + operational_intent_references: Optional[List[OperationalIntentReference]] = [] + """Operational intents in or near the subscription area at the time of creation/update, if `notify_for_operational_intents` is true.""" + + constraint_references: Optional[List[ConstraintReference]] = [] + """Constraints in or near the subscription area at the time of creation/update, if `notify_for_constraints` is true.""" + + +class DeleteSubscriptionResponse(ImplicitDict): + """Response for a successful request to delete a subscription.""" + + subscription: Subscription + + +class PutOperationalIntentReferenceParameters(ImplicitDict): + """Parameters for a request to create an OperationalIntentReference in the DSS. A subscription to changes overlapping this volume may be implicitly created, but this can be overridden by providing the (optional) 'subscription_id' to use. Note: The implicit subscription is managed by the DSS, not the USS.""" + + extents: List[Volume4D] + """Spacetime extents that bound this operational intent. + Start and end times, as well as lower and upper altitudes, are required for each volume. The end time may not be in the past. All volumes, both nominal and off-nominal, must be encompassed in these extents. However, these extents do not need to match the precise volumes of the operational intent; a single bounding extent may be provided instead, for instance. + """ + + key: Optional[Key] + """Proof that the USS creating or mutating this operational intent was aware of the current state of the airspace, with the expectation that this operational intent is therefore deconflicted from all relevant features in the airspace. This field is not required when declaring an operational intent Nonconforming or Contingent, or when there are no relevant Entities in the airspace, but is otherwise required. OVNs for constraints are required if and only if the USS managing this operational intent is performing the constraint processing role, which is indicated by whether the subscription associated with this operational intent triggers notifications for constraints. The key does not need to contain the OVN for the operational intent being updated.""" + + state: OperationalIntentState + + uss_base_url: OperationalIntentUssBaseURL + + subscription_id: Optional[EntityID] + """The ID of an existing subscription that the USS will use to keep the operator informed about updates to relevant airspace information. If this field is not provided when the operational intent is in the Activated, Nonconforming, or Contingent state, then the `new_subscription` field must be provided in order to provide notification capability for the operational intent. The subscription specified by this ID must cover at least the area over which this operational intent is conducted, and it must provide notifications for operational intents.""" + + new_subscription: Optional[ImplicitSubscriptionParameters] + """If an existing subscription is not specified in `subscription_id`, and the operational intent is in the Activated, Nonconforming, or Contingent state, then this field must be populated. When this field is populated, an implicit subscription will be created and associated with this operational intent, and will generally be deleted automatically upon the deletion of this operational intent.""" + + +class ChangeOperationalIntentReferenceResponse(ImplicitDict): + """Response to a request to create, update, or delete an OperationalIntentReference in the DSS.""" + + subscribers: List[SubscriberToNotify] = [] + """DSS subscribers that this client now has the obligation to notify of the operational intent changes just made. This client must call POST for each provided URL according to the USS-USS `/uss/v1/operational_intents` path API. The client's own subscriptions will also be included in this list.""" + + operational_intent_reference: OperationalIntentReference + + +class ChangeConstraintReferenceResponse(ImplicitDict): + """Response to a request to create, update, or delete a ConstraintReference. in the DSS.""" + + subscribers: List[SubscriberToNotify] = [] + """DSS subscribers that this client now has the obligation to notify of the constraint changes just made. This client must call POST for each provided URL according to the USS-USS `/uss/v1/constraints` path API. The client's own subscriptions will also be included in this list.""" + + constraint_reference: ConstraintReference + + +class GetOperationalIntentTelemetryResponse(ImplicitDict): + """Response to a peer request for telemetry of an off-nominal operational intent.""" + + operational_intent_id: EntityID + """ID of the operational intent which the vehicle reporting telemetry is flying.""" + + telemetry: Optional[VehicleTelemetry] + + next_telemetry_opportunity: Optional[Time] + """The next telemetry similar to this telemetry is not expected to be available until at or after this time, so the polling USS should generally not poll the endpoint providing this response data again until at or after that time. If this field is omitted, then there is no current expectation of new telemetry becoming available.""" + + +class USSLogSet(ImplicitDict): + """The set of log data fulfilling this standard's Logging requirements.""" + + messages: Optional[List[ExchangeRecord]] = [] + """Outgoing messages sent to other USSs and the DSS, and incoming messages received from other USSs, including instances where an expected response to a request is not received.""" + + operator_notifications: Optional[List[UserNotificationRecord]] = [] + """Instances of operator notifications as specifically required within this standard.""" + + operator_inputs: Optional[List[UserInputRecord]] = [] + """Instances of operator input as specifically required within this standard.""" + + operator_associations: Optional[List[OperatorAssociation]] = [] + """For a USS that manages operational intents, associations of an operator with operational intents that transitioned to the Accepted state.""" + + planning_attempts: Optional[List[PlanningRecord]] = [] + """For a USS that manages operational intents, instances where an operational intent could not be planned or replanned due to conflicts with other operational intents or constraints.""" + + operational_intent_positions: Optional[List[OperationalIntentPositions]] = [] + """For a USS performing conformance monitoring, all position data used for conformance monitoring that is ingested from the UA.""" + + constraint_provider_associations: Optional[List[ConstraintProviderAssociation]] = [] + """For a USS that performs constraint management, associations of an authorized constraint provider with all constraints that transition to the valid state.""" + + +class GeoZone(ImplicitDict): + """An airspace of defined dimensions, above the land areas or territorial waters of a State, within which a particular restriction or condition for UAS flights applies.""" + + identifier: CodeZoneIdentifierType + """A string of characters that uniquely identifies the UAS Zone within the State/Territory identified by the country attribute. + Note - The UAS Zone is uniquely identified worldwide by the combination of the country and the identifier attributes + """ + + country: CodeCountryISOType + """The State that has the authority to declare the zone. + Note - There will be no Zone belonging to two States. Not necessary to code the information that two zones are "in neighboring States" or "related". + """ + + zone_authority: List[Authority] + + name: Optional[TextShortType] + """A free text name by which the zone may be known by the public or by the UAS community.""" + + type: CodeZoneType + """An indication whether the Zone is provided with its common definition or with a customised definition, for a particular user.""" + + restriction: CodeRestrictionType + """An indication if flying in the zone is conditional, forbidden or unrestricted.""" + + restriction_conditions: Optional[List[ConditionExpressionType]] + """An indication of the conditions under which the zone can be used""" + + region: Optional[int] + """Where applicable, identifies a region inside a State where the UAS Zone is located. + Note 1) identified with a digit between 0-65535 (16 bit), corresponding to a list of regions pre-defined for each State. + Note 2) this attribute is intended to facilitate extracting sub-sets of data, for specific regions + """ + + reason: Optional[List[CodeZoneReasonType]] + """A coded indication for the reason that led to the establishment of the zone.""" + + other_reason_info: Optional[str] + """A free text description of the reason that led to the establishment of the zone, when not covered by a pre-defined coded value.""" + + regulation_exemption: Optional[CodeYesNoType] + """This is an extension point. It allows adding additional attributes of national interest through this element.""" + + u_space_class: Optional[CodeUSpaceClassType] + """A code that identifies the category or class of the zone applying a "USpace concept". + Note: Two (draft) classifications exist, one from Eurocontrol and one from CORUS. Therefore, two instances of this attribute are expected, one from each sub-list. This might be later replaced with separate attributes and separate lists of values. + """ + + message: Optional[TextShortType] + """A message to be displayed to the user of the zone, typically on the RPS for the Remote Pilot, to make him/her aware about specific information associated with the zone (typically when it is not only a restriction to fly in the zone, thus not only an alert or an automatic limitation, for example : “image capture prohibited in this zone”, “frequent strong winds in this zone”, “no landing or take-off in this zone”). This message is also used to indicate exemptions from regulation in a zone (see below). Several information can be grouped in a message, separated by a “/”.""" + + additional_properties: Optional[GeoZoneAdditional_properties] + """Indicates that exemptions from the national or European regulations are allowed in the UAS Zone, that will be detailed via the "message" property.""" + + +class ConstraintDetails(ImplicitDict): + """Details of a UTM constraint. Note that this data is not stored in the DSS; only with the clients.""" + + volumes: List[Volume4D] + """Volumes that wholly contain the constraint while being as small as practical. + The end time may not be in the past. + """ + + type: Optional[str] + """Type of airspace feature this constraint represents.""" + + geozone: Optional[GeoZone] + """If this constraint is an ED-269 compliant geo zone, the details about that geo zone.""" + + +class Constraint(ImplicitDict): + """Full specification of a UTM constraint.""" + + reference: ConstraintReference + + details: ConstraintDetails + + +class PutConstraintDetailsParameters(ImplicitDict): + """Parameters of a message informing of new full information for a constraint. Pushed (by a client, not the DSS) directly to clients with subscriptions when another client makes a change to airspace within a cell with a subscription.""" + + constraint_id: EntityID + """ID of constraint that has changed.""" + + constraint: Optional[Constraint] + """Full information about the constraint that has changed. If this field is omitted, the constraint was deleted. The `ovn` field in the nested `reference` must be populated.""" + + subscriptions: List[SubscriptionState] + """Subscription(s) prompting this notification.""" + + +class GetConstraintDetailsResponse(ImplicitDict): + """Response to peer request for the details of operational intent with the given ID.""" + + constraint: Constraint diff --git a/tools/openapi_conversion/Dockerfile b/tools/openapi_conversion/Dockerfile new file mode 100644 index 0000000..495e2cd --- /dev/null +++ b/tools/openapi_conversion/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.8-alpine + +ADD ./requirements.txt /app/requirements.txt +RUN pip install -r /app/requirements.txt +RUN rm -rf __pycache__ +ADD . /app +WORKDIR /app + +ENTRYPOINT ["python", "convert_openapi_to_implicitdict.py"] diff --git a/tools/openapi_conversion/convert_openapi_to_implicitdict.py b/tools/openapi_conversion/convert_openapi_to_implicitdict.py new file mode 100644 index 0000000..3c99912 --- /dev/null +++ b/tools/openapi_conversion/convert_openapi_to_implicitdict.py @@ -0,0 +1,40 @@ +# This tool generates Python data types from an OpenAPI YAML file. + +import argparse + +import yaml + +import data_types +import rendering + + +def _parse_args(): + parser = argparse.ArgumentParser(description='Autogenerate Python data types from an OpenAPI YAML') + + # Input/output specifications + parser.add_argument('--api', dest='api', type=str, + help='Source YAML to preprocess.') + parser.add_argument('--python_output', dest='python_output', type=str, + help='Output file for generated Python code') + + return parser.parse_args() + + +def main(): + args = _parse_args() + + # Parse OpenAPI + with open(args.api, mode='r') as f: + spec = yaml.full_load(f) + + # Parse data types + types = data_types.parse(spec) + + # Render Python code + with open(args.python_output, 'w') as f: + f.write(f'"""Data types from {spec["info"]["title"]} {spec["info"]["version"]} OpenAPI"""\n\n') + f.write('\n'.join(rendering.data_types(types))) + + +if __name__ == '__main__': + main() diff --git a/tools/openapi_conversion/data_types.py b/tools/openapi_conversion/data_types.py new file mode 100644 index 0000000..56f927d --- /dev/null +++ b/tools/openapi_conversion/data_types.py @@ -0,0 +1,238 @@ +import dataclasses +from typing import Any, Dict, List, Optional, Set, Tuple + + +@dataclasses.dataclass +class ObjectField: + """A data field within an Object data type""" + + api_name: str + """Name of the field in the API (generally snake cased)""" + + python_type: str + """The name of the Python data type which represents this field's value""" + + description: str + """Documentation of this field""" + + required: bool + """True if an instance of the parent object must specify a value for this field""" + + default: Optional[Any] + """Default value for field, if specified""" + + literal_default: bool = False + """If true, render the default without quotes even if it's a string""" + + +@dataclasses.dataclass +class DataType: + """A specific data type defined in the API""" + + name: str + """Name of this data type, as defined in the API""" + + python_type: str = '' + """Name of the Python data type ('ImplicitDict' for Object data types)""" + + description: str = '' + """Documentation of this data type""" + + fields: List[ObjectField] = dataclasses.field(default_factory=list) + """If this is an Object data type, a list of fields contained in that Object""" + + enum_values: List[str] = dataclasses.field(default_factory=list) + """If this is a enum data type, a list of values it may take on""" + + + +python_primitives: Dict[str, str] = { + 'string': 'str', + 'boolean': 'bool', +} +"""Maps OpenAPI `type` to Python primitive type""" + +python_numbers: Dict[str, str] = { + 'float': 'float', + 'double': 'float', + 'int32': 'int', + 'int64': 'int', + 'number': 'float', + 'integer': 'int', +} +"""Maps OpenAPI `format` (defaulting to `type` if `format` is missing) to Python primitive type""" + + +def is_primitive_python_type(python_type_name: str) -> bool: + """True iff python_type_name describes a built-in Python type""" + return python_type_name in python_primitives.values() or python_type_name in python_numbers.values() + + +def get_data_type_name(component_name: str, data_type_name: str) -> str: + """Get the plain data type name from a $ref URI. + + :param component_name: $ref URI to the data type of interest + :param data_type_name: context in which the data type is being retrieved (used for error message only) + :return: Plain data type name in the relative $ref URI + """ + if component_name == '': + return '' + elif component_name.startswith('#/components/schemas/'): + return component_name[len('#/components/schemas/'):] + else: + raise NotImplementedError('$ref expected to start with `#/components/schemas/`, but found `{}` instead for {}'.format(component_name, data_type_name)) + + +def _parse_referenced_type_name(schema: Dict, data_type_name: str) -> str: + options = schema['anyOf'] if 'anyOf' in schema else schema['allOf'] + if len(options) != 1: + raise NotImplementedError('Only one $ref is supported for anyOf and allOf; found {} elements instead'.format(len(options))) + option = options[0] + if not isinstance(option, dict): + raise ValueError('Expected dict entries in anyOf/allOf block; found {} instead'.format(option)) + if len(option) != 1 or '$ref' not in option: + raise NotImplementedError('The only element in anyOf/allOf must be a $ref dictionary; found {} instead'.format(option)) + return get_data_type_name(option['$ref'], data_type_name) + + +def make_object_field(python_object_name: str, api_field_name: str, schema: Dict, required: Set[str]) -> Tuple[ObjectField, List[DataType]]: + """Parse a single field in a data type or endpoint parameter schema. + + :param python_object_name: Name of the Python object containing this field, for error messages and inline type names + :param api_field_name: Name of the object field being parsed, according to the API + :param schema: Definition of the object field being parsed + :param required: The set of required fields for the parent object + :return: Tuple of + * The object field defined by the provided schema + * Any additional data types incidentally defined in the provided schema + """ + is_required = api_field_name in required + default_value = schema['default'] if 'default' in schema else None + if '$ref' in schema: + return ObjectField( + api_name=api_field_name, + python_type=get_data_type_name(schema['$ref'], python_object_name), + description=schema.get('description', ''), + required=is_required, + default=default_value), [] + elif 'anyOf' in schema or 'allOf' in schema: + return ObjectField( + api_name=api_field_name, + python_type=_parse_referenced_type_name(schema, python_object_name + '.' + api_field_name), + description=schema.get('description', ''), + required=is_required, + default=default_value), [] + else: + type_name = python_object_name + api_field_name[0].upper() + api_field_name[1:] + data_type, additional_types = make_data_types(type_name, schema) + if is_primitive_python_type(data_type.python_type) and not data_type.enum_values: + # No additional type declaration needed + if additional_types: + raise RuntimeError('{} field type `{}` was parsed as primitive {} but also generated {} additional types'.format(python_object_name, api_field_name, data_type.python_type, len(additional_types))) + field_data_type = data_type.python_type + elif data_type.python_type == 'StringBasedDateTime': + # No additional type declaration needed + field_data_type = data_type.python_type + elif data_type.python_type.startswith('List['): + # Use array data type as-is + field_data_type = data_type.python_type + else: + additional_types.append(data_type) + field_data_type = data_type.name + if len(data_type.enum_values) == 1 and default_value is None: + default_value = data_type.name + '.' + data_type.enum_values[0] + literal_default = True + else: + literal_default = False + return ObjectField( + api_name=api_field_name, + python_type=field_data_type, + description=data_type.description, + required=is_required, + default=default_value, + literal_default=literal_default), additional_types + + +def _make_object_fields(python_object_name: str, properties: Dict, required: Set[str]) -> Tuple[List[ObjectField], List[DataType]]: + fields: List[ObjectField] = [] + additional_types: List[DataType] = [] + for field_name, schema in properties.items(): + field, further_types = make_object_field(python_object_name, field_name, schema, required) + additional_types.extend(further_types) + fields.append(field) + return fields, additional_types + + +def make_data_types(api_name: str, schema: Dict) -> Tuple[DataType, List[DataType]]: + """Parse all data types necessary to express the provided data type schema. + + In addition to the primary data type described by `name`, this routine also + generates additional data types defined inline in the provided schema. + + :param api_name: Name of the primary data type being parsed, according to the API + :param schema: Definition of the data type being parsed + :return: Tuple of + * The primary data defined by the provided schema + * Any additional data types incidentally defined in the provided schema + """ + data_type = DataType(name=api_name) + additional_types = [] + + if 'description' in schema: + data_type.description = schema['description'] + + if 'type' in schema: + if schema['type'] in python_primitives: + data_type.python_type = python_primitives[schema['type']] + if data_type.python_type == 'str' and 'format' in schema and schema['format'] == 'date-time': + data_type.python_type = 'StringBasedDateTime' + elif schema['type'] in {'number', 'integer'}: + data_type.python_type = python_numbers.get(schema.get('format', schema['type']), '') + if not data_type.python_type: + raise ValueError('Unrecognized numeric format `{}` for {}'.format(schema.get('format', ''), api_name)) + elif schema['type'] == 'array': + if 'items' in schema: + items = schema['items'] + if '$ref' in items: + item_type_name = get_data_type_name(items['$ref'], api_name) + else: + item_type, further_types = make_data_types(api_name + 'Item', items) + additional_types.extend(further_types) + if item_type.description != '' or not is_primitive_python_type(item_type.python_type): + additional_types.append(item_type) + item_type_name = item_type.name + else: + item_type_name = item_type.python_type + data_type.python_type = f'List[{item_type_name}]' + else: + raise ValueError('Missing `items` declaration for {} array type'.format(api_name)) + elif schema['type'] == 'object': + data_type.python_type = 'ImplicitDict' + data_type.fields, further_types = _make_object_fields( + api_name, + schema.get('properties', {}), + set(schema.get('required', []))) + additional_types.extend(further_types) + else: + raise ValueError('Unrecognized type `{}` in {} type'.format(schema['type'], api_name)) + elif 'anyOf' in schema or 'allOf' in schema: + data_type.python_type = _parse_referenced_type_name(schema, api_name) + + if 'enum' in schema: + data_type.enum_values = schema['enum'] + + return data_type, additional_types + + +def parse(spec: dict) -> List[DataType]: + if 'components' not in spec: + raise ValueError('Missing `components` in YAML') + components = spec['components'] + if 'schemas' not in components: + raise ValueError('Missing `schemas` in `components`') + declared_types = [] + for name, schema in components['schemas'].items(): + data_type, additional_types = make_data_types(name, schema) + declared_types.extend(additional_types) + declared_types.append(data_type) + return declared_types diff --git a/tools/openapi_conversion/generate_apis.sh b/tools/openapi_conversion/generate_apis.sh new file mode 100755 index 0000000..bc4f173 --- /dev/null +++ b/tools/openapi_conversion/generate_apis.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +OS=$(uname) +if [[ $OS == "Darwin" ]]; then + # OSX uses BSD readlink + BASEDIR="$(dirname "$0")" +else + BASEDIR=$(readlink -e "$(dirname "$0")") +fi + +cd "${BASEDIR}" || exit + +docker image build -t openapi-python-converter . + +docker container run -it \ + -v "$(pwd)/../..:/resources" \ + openapi-python-converter \ + --api /resources/interfaces/astm/f3411/v19/remoteid/augmented.yaml \ + --python_output /resources/src/uas_standards/astm/f3411/v19/api.py + +docker container run -it \ + -v "$(pwd)/../..:/resources" \ + openapi-python-converter \ + --api /resources/interfaces/astm/f3411/v22a/remoteid/updated.yaml \ + --python_output /resources/src/uas_standards/astm/f3411/v22a/api.py + +docker container run -it \ + -v "$(pwd)/../..:/resources" \ + openapi-python-converter \ + --api /resources/interfaces/astm/f3548/v21/utm.yaml \ + --python_output /resources/src/uas_standards/astm/f3548/v21/api.py diff --git a/tools/openapi_conversion/rendering.py b/tools/openapi_conversion/rendering.py new file mode 100644 index 0000000..ad6d60f --- /dev/null +++ b/tools/openapi_conversion/rendering.py @@ -0,0 +1,177 @@ +import keyword +from typing import List + +from data_types import DataType, ObjectField, is_primitive_python_type + + +def docstring_comment(lines: List[str]) -> List[str]: + if len(lines) == 0: + return [] + if len(lines) == 1: + return [f'"""{lines[0]}"""'] + result = ['"""' + lines[0]] + result += lines[1:] + result += ['"""'] + return result + + +def indent(lines: List[str], level: int) -> List[str]: + """Indent each line. + + :param lines: Lines of text to be indented + :param level: Level of indent (each indent level is four spaces) + :return: Same lines of text provided after each line is indented + """ + if level == 0: + return lines + else: + return [' ' * level + line if line else '' for line in lines] + + +def data_type(d_type: DataType) -> List[str]: + """Generate Python code defining the provided data type. + + :param d_type: Parsed API data type to render into Python code + :return: Lines of Python code defining the provided data type + """ + docstring_lines = docstring_comment( + d_type.description.split('\n')) if d_type.description else [] + lines = [] + + if d_type.enum_values: + if any(str(v) in keyword.kwlist for v in d_type.enum_values): + lines.append(f'{d_type.name} = {d_type.python_type}') + docstring_lines = d_type.description.split( + '\n') if d_type.description else [] + docstring_lines += ['', 'Acceptable values:'] + ['* ' + str(v) for v + in + d_type.enum_values] + lines.extend(docstring_comment(docstring_lines)) + else: + lines.append(f'class {d_type.name}({d_type.python_type}, Enum):') + lines.extend(indent(docstring_lines, 1)) + if docstring_lines: + lines.append('') + + lines.extend( + indent([f'{v} = "{v}"' for v in d_type.enum_values], 1)) + elif is_primitive_python_type(d_type.python_type): + lines.append(f'{d_type.name} = {d_type.python_type}') + lines.extend(docstring_lines) + elif d_type.python_type == 'ImplicitDict': + if any(f.api_name in keyword.kwlist for f in d_type.fields): + lines.append(f'{d_type.name} = dict') + if d_type.description: + docstring_lines = d_type.description.split('\n') + docstring_lines.append('') + else: + docstring_lines = [] + docstring_lines.append('Expected keys:') + docstring_lines.extend('* ' + f.api_name for f in d_type.fields) + lines.extend(docstring_comment(docstring_lines)) + lines.append('') + else: + lines.append(f'class {d_type.name}(ImplicitDict):') + lines.extend(indent(docstring_lines, 1)) + if docstring_lines: + lines.append('') + + for field in d_type.fields: + lines.extend(indent(_object_field(field), 1)) + lines.append('') + if d_type.fields: + lines.pop() + else: + lines.append(f'{d_type.name} = {d_type.python_type}') + + return lines + + +def _object_field(field: ObjectField) -> List[str]: + """Generate an unindented definition of the provided field in Python code. + + :param field: Data type field to render into Python code + :return: Lines of Python code defining the provided field + """ + d_type = field.python_type if field.required else f'Optional[{field.python_type}]' + if field.default is not None: + if field.literal_default: + default_suffix = f' = {field.default}' + elif isinstance(field.default, str): + default_suffix = f' = "{field.default}"' + else: + default_suffix = f' = {str(field.default)}' + else: + default_suffix = '' + lines = [f'{field.api_name}: {d_type}{default_suffix}'] + if field.description: + lines.extend(docstring_comment(field.description.split('\n'))) + return lines + + +def data_types(d_types: List[DataType]) -> List[str]: + already_defined = [kw for kw in keyword.kwlist] + already_defined += ['int', 'float', 'complex', 'str', 'list', 'tuple', + 'range', 'bytes', 'bytearray', 'memoryview', 'dict', + 'bool', 'set', 'frozenset'] + + lines = ['# This file is autogenerated; do not modify manually!', ''] + lines.extend(['from __future__ import annotations', '']) + + if any(d.enum_values for d in d_types): + lines.append('from enum import Enum') + + basic_types = [] + if any(d.python_type.startswith('List[') for d in d_types) or any( + any(f.python_type.startswith('List[') for f in d.fields) for d in + d_types): + basic_types.append('List') + if any(any(not f.required for f in d.fields) for d in d_types): + basic_types.append('Optional') + lines.append('from typing import ' + ', '.join(basic_types)) + lines.append('') + + lines.append('from implicitdict import ImplicitDict') + if any(('StringBasedDateTime' in d.python_type) for d in d_types) or any( + any(('StringBasedDateTime' in f.python_type) for f in d.fields) for + d in d_types): + lines[-1] = lines[-1] + ', StringBasedDateTime' + already_defined.append('StringBasedDateTime') + already_defined.append('ImplicitDict') + + # Declare types in dependency order + total_defined = 0 + n_defined = 1 + + def _core_type(type_name): + core_type = type_name + while '[' in core_type: + core_type = core_type[core_type.index('[') + 1:] + while ']' in core_type: + core_type = core_type[0:core_type.index(']')] + return core_type + + while n_defined > 0: + n_defined = 0 + for d_type in d_types: + if d_type.name in already_defined: + continue + if _core_type(d_type.python_type) not in already_defined: + continue + if any((_core_type(f.python_type) not in already_defined) for f in + d_type.fields): + continue + + lines.extend(['', '']) + lines.extend(data_type(d_type)) + already_defined.append(d_type.name) + n_defined += 1 + total_defined += 1 + not_defined = [d_type for d_type in d_types if + d_type.name not in already_defined] + if not_defined: + not_defined_list = ', '.join(d_type.name for d_type in not_defined) + raise RuntimeError(f'Failed to define data types: {not_defined_list}') + + lines.append('') + return lines diff --git a/tools/openapi_conversion/requirements.txt b/tools/openapi_conversion/requirements.txt new file mode 100644 index 0000000..6a8382e --- /dev/null +++ b/tools/openapi_conversion/requirements.txt @@ -0,0 +1 @@ +pyyaml==5.4