From 9f06ab71f210a1e059669d6c210e377c78c91544 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 26 Oct 2022 15:10:53 -0700 Subject: [PATCH] Add InterUSS automated testing APIs (#5) --- .gitmodules | 3 + interfaces/interuss/automated_testing | 1 + src/uas_standards/interuss/__init__.py | 0 .../interuss/automated_testing/__init__.py | 0 .../flight_planning/__init__.py | 0 .../flight_planning/v1/__init__.py | 0 .../flight_planning/v1/api.py | 366 ++++++++++++++++++ .../geo_awareness/__init__.py | 0 .../geo_awareness/v1/__init__.py | 0 .../automated_testing/geo_awareness/v1/api.py | 181 +++++++++ .../automated_testing/rid/__init__.py | 0 .../automated_testing/rid/v1/__init__.py | 0 .../automated_testing/rid/v1/injection.py | 58 +++ .../automated_testing/rid/v1/observation.py | 68 ++++ tests/__init__.py | 0 tests/astm/__init__.py | 0 tests/astm/f3411/__init__.py | 0 tests/astm/f3411/v19/__init__.py | 0 tests/astm/f3411/v19/test_api.py | 5 + tests/astm/f3411/v22a/__init__.py | 0 tests/astm/f3411/v22a/test_api.py | 5 + tests/astm/f3548/__init__.py | 0 tests/astm/f3548/v21/__init__.py | 0 tests/astm/f3548/v21/test_api.py | 5 + tests/interuss/__init__.py | 0 tests/interuss/automated_testing/__init__.py | 0 .../flight_planning/__init__.py | 0 .../flight_planning/test_api.py | 5 + .../geo_awareness/__init__.py | 0 .../geo_awareness/v1/__init__.py | 0 .../geo_awareness/v1/test_api.py | 5 + .../automated_testing/rid/__init__.py | 0 .../automated_testing/rid/v1/__init__.py | 0 .../rid/v1/test_injection.py | 5 + .../rid/v1/test_observation.py | 5 + .../convert_openapi_to_implicitdict.py | 5 +- tools/openapi_conversion/data_types.py | 30 +- tools/openapi_conversion/generate_apis.sh | 32 ++ tools/openapi_conversion/rendering.py | 42 +- 39 files changed, 809 insertions(+), 12 deletions(-) create mode 160000 interfaces/interuss/automated_testing create mode 100644 src/uas_standards/interuss/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/flight_planning/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/flight_planning/v1/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/flight_planning/v1/api.py create mode 100644 src/uas_standards/interuss/automated_testing/geo_awareness/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/geo_awareness/v1/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/geo_awareness/v1/api.py create mode 100644 src/uas_standards/interuss/automated_testing/rid/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/rid/v1/__init__.py create mode 100644 src/uas_standards/interuss/automated_testing/rid/v1/injection.py create mode 100644 src/uas_standards/interuss/automated_testing/rid/v1/observation.py create mode 100644 tests/__init__.py create mode 100644 tests/astm/__init__.py create mode 100644 tests/astm/f3411/__init__.py create mode 100644 tests/astm/f3411/v19/__init__.py create mode 100644 tests/astm/f3411/v19/test_api.py create mode 100644 tests/astm/f3411/v22a/__init__.py create mode 100644 tests/astm/f3411/v22a/test_api.py create mode 100644 tests/astm/f3548/__init__.py create mode 100644 tests/astm/f3548/v21/__init__.py create mode 100644 tests/astm/f3548/v21/test_api.py create mode 100644 tests/interuss/__init__.py create mode 100644 tests/interuss/automated_testing/__init__.py create mode 100644 tests/interuss/automated_testing/flight_planning/__init__.py create mode 100644 tests/interuss/automated_testing/flight_planning/test_api.py create mode 100644 tests/interuss/automated_testing/geo_awareness/__init__.py create mode 100644 tests/interuss/automated_testing/geo_awareness/v1/__init__.py create mode 100644 tests/interuss/automated_testing/geo_awareness/v1/test_api.py create mode 100644 tests/interuss/automated_testing/rid/__init__.py create mode 100644 tests/interuss/automated_testing/rid/v1/__init__.py create mode 100644 tests/interuss/automated_testing/rid/v1/test_injection.py create mode 100644 tests/interuss/automated_testing/rid/v1/test_observation.py diff --git a/.gitmodules b/.gitmodules index 784c741..0c40dac 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "interfaces/astm/f3548/v21"] path = interfaces/astm/f3548/v21 url = https://github.com/astm-utm/Protocol +[submodule "interfaces/interuss/automated_testing"] + path = interfaces/interuss/automated_testing + url = https://github.com/interuss/automated_testing_interfaces diff --git a/interfaces/interuss/automated_testing b/interfaces/interuss/automated_testing new file mode 160000 index 0000000..fa3a5f5 --- /dev/null +++ b/interfaces/interuss/automated_testing @@ -0,0 +1 @@ +Subproject commit fa3a5f544161c408f50255630a23b670c74a67d1 diff --git a/src/uas_standards/interuss/__init__.py b/src/uas_standards/interuss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/__init__.py b/src/uas_standards/interuss/automated_testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/flight_planning/__init__.py b/src/uas_standards/interuss/automated_testing/flight_planning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/flight_planning/v1/__init__.py b/src/uas_standards/interuss/automated_testing/flight_planning/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/flight_planning/v1/api.py b/src/uas_standards/interuss/automated_testing/flight_planning/v1/api.py new file mode 100644 index 0000000..cab4415 --- /dev/null +++ b/src/uas_standards/interuss/automated_testing/flight_planning/v1/api.py @@ -0,0 +1,366 @@ +"""Data types from Strategic Coordination Test Data Injection 0.0.1 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 StatusResponseStatus(str, Enum): + """The status of the USS automated testing interface. + - `Starting`: the USS is starting and the automated test driver should wait before sending requests. + - `Ready`: the USS is ready to receive test requests. + """ + + Starting = "Starting" + Ready = "Ready" + + +class StatusResponse(ImplicitDict): + status: StatusResponseStatus + """The status of the USS automated testing interface. + - `Starting`: the USS is starting and the automated test driver should wait before sending requests. + - `Ready`: the USS is ready to receive test requests. + """ + + version: Optional[str] + """Arbitrary string representing the version of the USS system to be tested.""" + + +UUIDv4Format = str +"""String whose format matches a version-4 UUID according to RFC 4122.""" + + +EntityID = UUIDv4Format + + +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 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" + + +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 FlightAuthorisationDataOperation_category(str, Enum): + """Category of UAS operation (‘open’, ‘specific’, ‘certified’) as defined in COMMISSION DELEGATED REGULATION (EU) 2019/945. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 4.""" + + Unknown = "Unknown" + Open = "Open" + Specific = "Specific" + Certified = "Certified" + + +class OperationMode(str, Enum): + """Specify if the operation is a `VLOS` or `BVLOS` operation. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 2.""" + + Undeclared = "Undeclared" + Vlos = "Vlos" + Bvlos = "Bvlos" + + +class UASClass(str, Enum): + """Specify the class of the UAS to be flown, the specifition matches EASA class identification label categories. UAS aircraft class as defined in COMMISSION DELEGATED REGULATION (EU) 2019/945 (C0 to C4) and COMMISSION DELEGATED REGULATION (EU) 2020/1058 (C5 and C6). This field is required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 4.""" + + Other = "Other" + C0 = "C0" + C1 = "C1" + C2 = "C2" + C3 = "C3" + C4 = "C4" + C5 = "C5" + C6 = "C6" + + +class InjectFlightResponseResult(str, Enum): + """The result of the flight submission + + - `Planned`: The flight submission data was valid and the flight was successfully processed by the USS and is now authorized. + + - `ReadyToFly`: The flight is ready for the operator to begin flying. + + - `Rejected`: The flight submission data provided was invalid and/or could not be used to attempt to authorize the flight. + + - `ConflictWithFlight`: The flight submission data was valid, but the flight could not be authorized because of a disallowed conflict with another flight. + + - `Failed`: The USS was not able to successfully authorize the flight due to a problem with the USS or a downstream system + """ + + Planned = "Planned" + ReadyToFly = "ReadyToFly" + Rejected = "Rejected" + ConflictWithFlight = "ConflictWithFlight" + Failed = "Failed" + + +class InjectFlightResponse(ImplicitDict): + result: InjectFlightResponseResult + """The result of the flight submission + + - `Planned`: The flight submission data was valid and the flight was successfully processed by the USS and is now authorized. + + - `ReadyToFly`: The flight is ready for the operator to begin flying. + + - `Rejected`: The flight submission data provided was invalid and/or could not be used to attempt to authorize the flight. + + - `ConflictWithFlight`: The flight submission data was valid, but the flight could not be authorized because of a disallowed conflict with another flight. + + - `Failed`: The USS was not able to successfully authorize the flight due to a problem with the USS or a downstream system + """ + + notes: Optional[str] + """Human-readable explanation of the observed result. This explanation should be available to a human reviewing the test results, and ideally should explain why an undesirable result was obtained. For instance, if the injection attempt Failed, then these notes may indicate that the attempt failed because the DSS indicated 400 to a valid request (perhaps also including the valid request as proof).""" + + operational_intent_id: Optional[EntityID] + """The id of the operational intent communicated to the DSS. This value is only required when the result of the flight submission is `Planned`.""" + + +class DeleteFlightResponseResult(str, Enum): + """The result of attempted flight cancellation/closure + + - `Closed`: The flight was closed successfully by the USS and is now out of the UTM system. + + - `Failed`: The flight could not be closed successfully by the USS. + """ + + Closed = "Closed" + Failed = "Failed" + + +class DeleteFlightResponse(ImplicitDict): + result: DeleteFlightResponseResult + """The result of attempted flight cancellation/closure + + - `Closed`: The flight was closed successfully by the USS and is now out of the UTM system. + + - `Failed`: The flight could not be closed successfully by the USS. + """ + + notes: Optional[str] + """Human-readable explanation of the observed result.""" + + +class ClearAreaOutcome(ImplicitDict): + success: Optional[bool] = False + """True if, and only if, all flights in the specified area owned by the USS were canceled and removed.""" + + message: Optional[str] + """If the USS was unable to clear the entire area, this message can provide information on the problem encountered.""" + + timestamp: str + """The time at which this operation was performed by the USS.""" + + +class ClearAreaResponse(ImplicitDict): + outcome: ClearAreaOutcome + + +class Capability(str, Enum): + """Capability of a USS. + + `FlightAuthorisationValidation`: USS supports EU flight authorisation + parameter validation. + + `BasicStrategicConflictDetection`: USS supports strategic conflict + detection for typical flights, including future planning (Accepted + operational intents), activation (Accepted operational intents), and + closing (deleting the operational intent reference). + + `HighPriorityFlights`: USS supports flights at priority levels higher + than typical flights. + """ + + FlightAuthorisationValidation = "FlightAuthorisationValidation" + BasicStrategicConflictDetection = "BasicStrategicConflictDetection" + HighPriorityFlights = "HighPriorityFlights" + + +class CapabilitiesResponse(ImplicitDict): + capabilities: Optional[List[Capability]] = [] + """Set of capabilities supported by this USS.""" + + +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 OperationalIntentTestInjection(ImplicitDict): + """Parameters that define an operational intent: this injection is used to create a operational intent reference in the DSS and also responding to requests for details of that operational intent (by other USSes or the test driver). The USS under test will need to process this data to both create a valid operational intent reference and responding to a query for details.""" + + state: OperationalIntentState + + priority: Priority + + volumes: List[Volume4D] + """Nominal volumes, as would be reported by a USS's operational_intents endpoint.""" + + off_nominal_volumes: List[Volume4D] + """Off-Nominal volumes, as would be reported by a USS's operational_intents endpoint.""" + + +class FlightAuthorisationData(ImplicitDict): + """A dataset to hold details of a UAS flight authorization request. Full description of a flight authorisation including mandatory information required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664 for an UAS flight authorisation request. Reference: https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32021R0664&from=EN#d1e32-178-1""" + + uas_serial_number: str + """Unique serial number of the unmanned aircraft or, if the unmanned aircraft is privately built, the unique serial number of the add-on. This is expressed in the ANSI/CTA-2063 Physical Serial Number format. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 1.""" + + operation_mode: OperationMode + + operation_category: FlightAuthorisationDataOperation_category + """Category of UAS operation (‘open’, ‘specific’, ‘certified’) as defined in COMMISSION DELEGATED REGULATION (EU) 2019/945. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 4.""" + + uas_class: UASClass + + identification_technologies: List[str] + """Technology used to identify the UAS. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 6.""" + + uas_type_certificate: Optional[str] + """Provisional field. Not applicable as of September 2021. Required only if `uas_class` is set to `other` by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 4.""" + + connectivity_methods: List[str] + """Connectivity methods. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 7.""" + + endurance_minutes: int + """Endurance of the UAS. This is expressed in minutes. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 8.""" + + emergency_procedure_url: str + """The URL at which the applicable emergency procedure in case of a loss of command and control link may be retrieved. Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 9.""" + + operator_id: str + """Registration number of the UAS operator. + The format is defined in EASA Easy Access Rules for Unmanned Aircraft Systems GM1 to AMC1 + Article 14(6) Registration of UAS operators and ‘certified’ UAS. + Required by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 10. + """ + + uas_id: Optional[str] + """When applicable, the registration number of the unmanned aircraft. + This is expressed using the nationality and registration mark of the unmanned aircraft in + line with ICAO Annex 7. + Specified by ANNEX IV of COMMISSION IMPLEMENTING REGULATION (EU) 2021/664, paragraph 10. + """ + + +class InjectFlightRequest(ImplicitDict): + operational_intent: Optional[OperationalIntentTestInjection] + + flight_authorisation: Optional[FlightAuthorisationData] + + +class ClearAreaRequest(ImplicitDict): + request_id: str + """Unique string identifying this request. If a second request with an identical ID is received, the USS may return the same response from the previous operation rather than attempting to clear the area again (the USS may also attempt to clear the area again).""" + + extent: Volume4D + """The USS should cancel and remove any flight where any part of that flight intersects this area.""" diff --git a/src/uas_standards/interuss/automated_testing/geo_awareness/__init__.py b/src/uas_standards/interuss/automated_testing/geo_awareness/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/geo_awareness/v1/__init__.py b/src/uas_standards/interuss/automated_testing/geo_awareness/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/geo_awareness/v1/api.py b/src/uas_standards/interuss/automated_testing/geo_awareness/v1/api.py new file mode 100644 index 0000000..8b63946 --- /dev/null +++ b/src/uas_standards/interuss/automated_testing/geo_awareness/v1/api.py @@ -0,0 +1,181 @@ +"""Data types from Geo-Awareness Automated Test Interfaces 0.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 + + +UUIDv4Format = str +"""String whose format matches a version-4 UUID according to RFC 4122.""" + + +class StatusResponseStatus(str, Enum): + """The status of the USS automated testing interface. + - `Starting`: the USS is starting and the automated test driver should wait before sending requests. + - `Ready`: the USS is ready to receive test requests. + """ + + Starting = "Starting" + Ready = "Ready" + + +class StatusResponse(ImplicitDict): + status: StatusResponseStatus + """The status of the USS automated testing interface. + - `Starting`: the USS is starting and the automated test driver should wait before sending requests. + - `Ready`: the USS is ready to receive test requests. + """ + + version: Optional[str] + """Arbitrary string representing the version of the USS system to be tested.""" + + +class GeozoneHttpsSourceFormat(str, Enum): + """The format of the response expected from the source.""" + + ED_269 = "ED-269" + + +class GeozoneHttpsSource(ImplicitDict): + url: str + """The URL at which the Geozone data shall be downloaded from.""" + + format: Optional[GeozoneHttpsSourceFormat] = GeozoneHttpsSourceFormat.ED_269 + """The format of the response expected from the source.""" + + +class GeozoneSourceResponseResult(str, Enum): + """The status of the Geozone source and the handling of its data by the USS. + - `Activating`: the USS is processing the request and is currently activating the Geozone data. + - `Ready`: the Geozone data has been successfully activated and the USS is ready to receive test requests. + - `Deactivating`: the Geozone data is being deactivated. + - `Unsupported`: the USS cannot process the dataset type specified. + - `Rejected`: the Geozone data was rejected because it is invalid. + - `Error`: the Geozone data activation or deactivation failed. The message field is required in this case. + """ + + Activating = "Activating" + Ready = "Ready" + Deactivating = "Deactivating" + Unsupported = "Unsupported" + Rejected = "Rejected" + Error = "Error" + + +class GeozoneSourceResponse(ImplicitDict): + result: GeozoneSourceResponseResult + """The status of the Geozone source and the handling of its data by the USS. + - `Activating`: the USS is processing the request and is currently activating the Geozone data. + - `Ready`: the Geozone data has been successfully activated and the USS is ready to receive test requests. + - `Deactivating`: the Geozone data is being deactivated. + - `Unsupported`: the USS cannot process the dataset type specified. + - `Rejected`: the Geozone data was rejected because it is invalid. + - `Error`: the Geozone data activation or deactivation failed. The message field is required in this case. + """ + + message: Optional[str] + """Human-readable explanation of the result for debugging purpose only. This field is required when the result value is `Error`.""" + + +class UomDimensions(str, Enum): + M = "M" + FT = "FT" + + +class VerticalReferenceType(str, Enum): + AGL = "AGL" + AMSL = "AMSL" + + +USpaceClass = str + + +class Restriction(str, Enum): + PROHIBITED = "PROHIBITED" + REQ_AUTHORISATION = "REQ_AUTHORISATION" + CONDITIONAL = "CONDITIONAL" + NO_RESTRICTION = "NO_RESTRICTION" + + +class Position(ImplicitDict): + uomDimensions: UomDimensions + + verticalReferenceType: VerticalReferenceType + + height: Optional[float] = 0 + """Height above vertical reference datum indicated in `verticalReferenceType`, in units of `uomDimensions`.""" + + longitude: Optional[float] = 0 + """Longitude, degrees east of prime meridian.""" + + latitude: Optional[float] = 0 + """Latitude, degrees north of the equator.""" + + +class ED269Filters(ImplicitDict): + """Filter criteria for the selection of Geozones according to ED-269 characteristics.""" + + uSpaceClass: Optional[USpaceClass] + """If specified, only select Geozones which are of the specified `uSpaceClass`.""" + + acceptableRestrictions: Optional[List[Restriction]] + """If specified and non-empty, only select Geozones which are one of the specified restriction types.""" + + +class GeozonesCheckResultGeozone(str, Enum): + """Indication of whether one or more applicable Geozones were selected according to the selection criteria of the corresponding check. + * Present: One or more applicable Geozones were selected. * Absent: No applicable Geozones were selected. * UnsupportedFilter: Applicable Geozones could not be selected because one or more filter criteria are not supported by the USSP. If this value is specified, `message` must be populated. * Error: An error or condition not enumerated above occurred. If this value is specified, `message` must be populated. + """ + + Present = "Present" + Absent = "Absent" + UnsupportedFilter = "UnsupportedFilter" + Error = "Error" + + +class GeozonesCheckResult(ImplicitDict): + geozone: GeozonesCheckResultGeozone + """Indication of whether one or more applicable Geozones were selected according to the selection criteria of the corresponding check. + * Present: One or more applicable Geozones were selected. * Absent: No applicable Geozones were selected. * UnsupportedFilter: Applicable Geozones could not be selected because one or more filter criteria are not supported by the USSP. If this value is specified, `message` must be populated. * Error: An error or condition not enumerated above occurred. If this value is specified, `message` must be populated. + """ + + message: Optional[str] + """A human-readable description of why the non-standard `geozone` value was reported. Should only be populated when appropriate according to the value of the `geozone` field.""" + + +class CreateGeozoneSourceRequest(ImplicitDict): + https_source: Optional[GeozoneHttpsSource] + + +class GeozonesFilterSet(ImplicitDict): + """Set of filters to select only a subset of Geozones. Only Geozones which are applicable to all specified filters within this filter set should be selected.""" + + position: Optional[Position] + """If specified, only select Geozones encompassing this position.""" + + after: Optional[StringBasedDateTime] + """If specified, only select Geozones which encompass at least some times at or after this time.""" + + before: Optional[StringBasedDateTime] + """If specified, only select Geozones which encompass at least some times at or before this time.""" + + ed269: Optional[ED269Filters] + + +class GeozonesCheckReply(ImplicitDict): + applicableGeozone: Optional[List[GeozonesCheckResult]] = [] + """Responses to each of the `checks` in the request. The number of entries in this array should match the number of entries in the `checks` field of the request.""" + + +class GeozonesCheck(ImplicitDict): + filterSets: List[GeozonesFilterSet] + """Select Geozones which match any of the specified filter sets.""" + + +class GeozonesCheckRequest(ImplicitDict): + checks: List[GeozonesCheck] diff --git a/src/uas_standards/interuss/automated_testing/rid/__init__.py b/src/uas_standards/interuss/automated_testing/rid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/rid/v1/__init__.py b/src/uas_standards/interuss/automated_testing/rid/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/uas_standards/interuss/automated_testing/rid/v1/injection.py b/src/uas_standards/interuss/automated_testing/rid/v1/injection.py new file mode 100644 index 0000000..7078726 --- /dev/null +++ b/src/uas_standards/interuss/automated_testing/rid/v1/injection.py @@ -0,0 +1,58 @@ +"""Data types from Remote ID Test Data Injection 0.0.1 OpenAPI""" + +# This file is autogenerated; do not modify manually! + +from __future__ import annotations + +from typing import List + +from implicitdict import ImplicitDict + + +from uas_standards.astm.f3411.v19.api import RIDFlightDetails + + +class TestFlightDetails(ImplicitDict): + """The set of data with which the Service Provider system under test should respond when queried for the details of a test flight.""" + + effective_after: str + """The time after which the Service Provider system under test should respond with `details`, unless other `details` with a more recent `effective_after` time (before the current time) are available.""" + + details: RIDFlightDetails + """The details of the flight. Follows the RIDFlightDetails schema from the ASTM remote ID standard.""" + + +from uas_standards.astm.f3411.v19.api import RIDAircraftState + + +class TestFlight(ImplicitDict): + """The set of data to be injected into a Service Provider system under test for a single flight.""" + + injection_id: str + """ID of the injected test flight. Remains the same regardless of the flight ID/UTM ID reported in the system.""" + + telemetry: List[RIDAircraftState] + """The set of telemetry data that should be injected into the system for this flight. Each element follows the RIDAircraftState schema from the ASTM remote ID standard.""" + + details_responses: List[TestFlightDetails] + """The details of the flight as a function of time.""" + + +class CreateTestParameters(ImplicitDict): + """A complete set of data to be injected into a Service Provider system under test.""" + + requested_flights: List[TestFlight] + """One or more logical flights, each containing test data to inject into the system. Elements should be sorted in ascending order of `timestamp`.""" + + +class ChangeTestResponse(ImplicitDict): + injected_flights: List[TestFlight] + """The complete set of test data actually injected into the Service Provider system under test.""" + + version: str + """Version of test. Used to delete test.""" + + +class DeleteTestResponse(ImplicitDict): + injected_flights: List[TestFlight] + """The complete set of test data deleted.""" diff --git a/src/uas_standards/interuss/automated_testing/rid/v1/observation.py b/src/uas_standards/interuss/automated_testing/rid/v1/observation.py new file mode 100644 index 0000000..6720e4e --- /dev/null +++ b/src/uas_standards/interuss/automated_testing/rid/v1/observation.py @@ -0,0 +1,68 @@ +"""Data types from Remote ID Display Data Observation 0.0.1 OpenAPI""" + +# This file is autogenerated; do not modify manually! + +from __future__ import annotations + +from typing import List, Optional + +from implicitdict import ImplicitDict + + +class GetDetailsResponse(ImplicitDict): + """Response to a request to get details about a flight.""" + + + +class Position(ImplicitDict): + """A position on Earth.""" + + lat: float + """Degrees of latitude north of the equator, with reference to the WGS84 ellipsoid.""" + + lng: float + """Degrees of longitude east of the Prime Meridian, with reference to the WGS84 ellipsoid.""" + + alt: Optional[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.""" + + +class Path(ImplicitDict): + """Path followed by a flight.""" + + positions: List[Position] + """Sequential positions available for a flight.""" + + +class Flight(ImplicitDict): + id: str + """Identifier of flight that may be used to obtain details about the flight. This is not necessarily the UTM/flight ID in the remote ID system.""" + + most_recent_position: Optional[Position] + """Most recent position known for the flight.""" + + recent_paths: Optional[List[Path]] + """Paths the flight recently traveled, if available.""" + + +class Cluster(ImplicitDict): + """A general area containing one or more flight.""" + + corners: List[Position] + """Two opposite corners of a rectangular lat-lng box bounding the cluster.""" + + area_sqm: float + """Area of the cluster in square meters.""" + + number_of_flights: int + """Number of flights within the cluster.""" + + +class GetDisplayDataResponse(ImplicitDict): + """Response to a request for current data that would be visualized by a Display Application.""" + + flights: Optional[List[Flight]] = [] + """Current information for set of discovered flights whose precise locations are known.""" + + clusters: Optional[List[Cluster]] = [] + """Current information for sets of discovered flights whose precise locations are not known.""" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/astm/__init__.py b/tests/astm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/astm/f3411/__init__.py b/tests/astm/f3411/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/astm/f3411/v19/__init__.py b/tests/astm/f3411/v19/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/astm/f3411/v19/test_api.py b/tests/astm/f3411/v19/test_api.py new file mode 100644 index 0000000..823bb90 --- /dev/null +++ b/tests/astm/f3411/v19/test_api.py @@ -0,0 +1,5 @@ +from uas_standards.astm.f3411.v19.api import * + + +def test_import(): + pass diff --git a/tests/astm/f3411/v22a/__init__.py b/tests/astm/f3411/v22a/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/astm/f3411/v22a/test_api.py b/tests/astm/f3411/v22a/test_api.py new file mode 100644 index 0000000..a87aadd --- /dev/null +++ b/tests/astm/f3411/v22a/test_api.py @@ -0,0 +1,5 @@ +from uas_standards.astm.f3411.v22a.api import * + + +def test_import(): + pass diff --git a/tests/astm/f3548/__init__.py b/tests/astm/f3548/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/astm/f3548/v21/__init__.py b/tests/astm/f3548/v21/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/astm/f3548/v21/test_api.py b/tests/astm/f3548/v21/test_api.py new file mode 100644 index 0000000..2d322bf --- /dev/null +++ b/tests/astm/f3548/v21/test_api.py @@ -0,0 +1,5 @@ +from uas_standards.astm.f3548.v21.api import * + + +def test_import(): + pass diff --git a/tests/interuss/__init__.py b/tests/interuss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/interuss/automated_testing/__init__.py b/tests/interuss/automated_testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/interuss/automated_testing/flight_planning/__init__.py b/tests/interuss/automated_testing/flight_planning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/interuss/automated_testing/flight_planning/test_api.py b/tests/interuss/automated_testing/flight_planning/test_api.py new file mode 100644 index 0000000..269910a --- /dev/null +++ b/tests/interuss/automated_testing/flight_planning/test_api.py @@ -0,0 +1,5 @@ +from uas_standards.interuss.automated_testing.flight_planning.v1.api import * + + +def test_import(): + pass diff --git a/tests/interuss/automated_testing/geo_awareness/__init__.py b/tests/interuss/automated_testing/geo_awareness/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/interuss/automated_testing/geo_awareness/v1/__init__.py b/tests/interuss/automated_testing/geo_awareness/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/interuss/automated_testing/geo_awareness/v1/test_api.py b/tests/interuss/automated_testing/geo_awareness/v1/test_api.py new file mode 100644 index 0000000..13409a7 --- /dev/null +++ b/tests/interuss/automated_testing/geo_awareness/v1/test_api.py @@ -0,0 +1,5 @@ +from uas_standards.interuss.automated_testing.geo_awareness.v1.api import * + + +def test_import(): + pass diff --git a/tests/interuss/automated_testing/rid/__init__.py b/tests/interuss/automated_testing/rid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/interuss/automated_testing/rid/v1/__init__.py b/tests/interuss/automated_testing/rid/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/interuss/automated_testing/rid/v1/test_injection.py b/tests/interuss/automated_testing/rid/v1/test_injection.py new file mode 100644 index 0000000..645fafb --- /dev/null +++ b/tests/interuss/automated_testing/rid/v1/test_injection.py @@ -0,0 +1,5 @@ +from uas_standards.interuss.automated_testing.rid.v1.injection import * + + +def test_import(): + pass diff --git a/tests/interuss/automated_testing/rid/v1/test_observation.py b/tests/interuss/automated_testing/rid/v1/test_observation.py new file mode 100644 index 0000000..b64e938 --- /dev/null +++ b/tests/interuss/automated_testing/rid/v1/test_observation.py @@ -0,0 +1,5 @@ +from uas_standards.interuss.automated_testing.rid.v1.observation import * + + +def test_import(): + pass diff --git a/tools/openapi_conversion/convert_openapi_to_implicitdict.py b/tools/openapi_conversion/convert_openapi_to_implicitdict.py index 3c99912..7d016a2 100644 --- a/tools/openapi_conversion/convert_openapi_to_implicitdict.py +++ b/tools/openapi_conversion/convert_openapi_to_implicitdict.py @@ -16,6 +16,9 @@ def _parse_args(): help='Source YAML to preprocess.') parser.add_argument('--python_output', dest='python_output', type=str, help='Output file for generated Python code') + parser.add_argument('--default_package', dest='default_package', type=str, + help='If this API refers to objects in another API, the Python package name where those other objects may be found', + default='') return parser.parse_args() @@ -33,7 +36,7 @@ def main(): # 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))) + f.write('\n'.join(rendering.data_types(types, args.default_package))) if __name__ == '__main__': diff --git a/tools/openapi_conversion/data_types.py b/tools/openapi_conversion/data_types.py index 56f927d..5d0d57c 100644 --- a/tools/openapi_conversion/data_types.py +++ b/tools/openapi_conversion/data_types.py @@ -41,8 +41,8 @@ class DataType: 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""" + enum_values: Dict[str, str] = dataclasses.field(default_factory=dict) + """If this is a enum data type, a map from values it may take on to Python names for those values""" @@ -80,7 +80,11 @@ def get_data_type_name(component_name: str, data_type_name: str) -> str: 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)) + if '#/components/schemas/' not in component_name: + raise ValueError('$ref expected to contain `#/components/schemas/`, but found `{}` instead for {}'.format(component_name, data_type_name)) + name = get_data_type_name(component_name[component_name.index('#'):], data_type_name) + print(f'WARNING: Assuming the variable type of {component_name} should be "{name}" and that it will be manually declared') + return name def _parse_referenced_type_name(schema: Dict, data_type_name: str) -> str: @@ -140,7 +144,7 @@ def make_object_field(python_object_name: str, api_field_name: str, schema: Dict 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] + default_value = data_type.name + '.' + next(iter(data_type.enum_values.values())) literal_default = True else: literal_default = False @@ -163,6 +167,19 @@ def _make_object_fields(python_object_name: str, properties: Dict, required: Set return fields, additional_types +def _make_python_enums(values: List[str]) -> Dict[str, str]: + valid_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' + valid_start_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_' + result = {} + for value in values: + name = str(value) + if name[0] not in valid_start_characters: + name = '_' + name + name = ''.join(c if c in valid_characters else '_' for c in name) + result[str(value)] = name + return result + + 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. @@ -219,7 +236,10 @@ def make_data_types(api_name: str, schema: Dict) -> Tuple[DataType, List[DataTyp data_type.python_type = _parse_referenced_type_name(schema, api_name) if 'enum' in schema: - data_type.enum_values = schema['enum'] + data_type.enum_values = _make_python_enums(schema['enum']) + + if not data_type.python_type: + data_type.python_type = 'str' return data_type, additional_types diff --git a/tools/openapi_conversion/generate_apis.sh b/tools/openapi_conversion/generate_apis.sh index bc4f173..1f6928e 100755 --- a/tools/openapi_conversion/generate_apis.sh +++ b/tools/openapi_conversion/generate_apis.sh @@ -12,20 +12,52 @@ cd "${BASEDIR}" || exit docker image build -t openapi-python-converter . +echo "F3411-19" 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 +echo "F3411-22a" 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 +echo "F3548-21" 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 + +echo "Geo-awareness automated testing" +docker container run -it \ + -v "$(pwd)/../..:/resources" \ + openapi-python-converter \ + --api /resources/interfaces/interuss/automated_testing/geo-awareness/v1/geo-awareness.yaml \ + --python_output /resources/src/uas_standards/interuss/automated_testing/geo_awareness/v1/api.py + +echo "RID injection automated testing" +docker container run -it \ + -v "$(pwd)/../..:/resources" \ + openapi-python-converter \ + --api /resources/interfaces/interuss/automated_testing/rid/v1/injection.yaml \ + --python_output /resources/src/uas_standards/interuss/automated_testing/rid/v1/injection.py \ + --default_package uas_standards.astm.f3411.v19.api + +echo "RID observation automated testing" +docker container run -it \ + -v "$(pwd)/../..:/resources" \ + openapi-python-converter \ + --api /resources/interfaces/interuss/automated_testing/rid/v1/observation.yaml \ + --python_output /resources/src/uas_standards/interuss/automated_testing/rid/v1/observation.py + +echo "Flight planning automated testing" +docker container run -it \ + -v "$(pwd)/../..:/resources" \ + openapi-python-converter \ + --api /resources/interfaces/interuss/automated_testing/scd/v1/scd.yaml \ + --python_output /resources/src/uas_standards/interuss/automated_testing/flight_planning/v1/api.py diff --git a/tools/openapi_conversion/rendering.py b/tools/openapi_conversion/rendering.py index ad6d60f..18b654b 100644 --- a/tools/openapi_conversion/rendering.py +++ b/tools/openapi_conversion/rendering.py @@ -39,7 +39,7 @@ def data_type(d_type: DataType) -> List[str]: lines = [] if d_type.enum_values: - if any(str(v) in keyword.kwlist for v in d_type.enum_values): + if any(str(t) in keyword.kwlist for v, t in d_type.enum_values.items()): lines.append(f'{d_type.name} = {d_type.python_type}') docstring_lines = d_type.description.split( '\n') if d_type.description else [] @@ -54,7 +54,7 @@ def data_type(d_type: DataType) -> List[str]: lines.append('') lines.extend( - indent([f'{v} = "{v}"' for v in d_type.enum_values], 1)) + indent([f'{t} = "{v}"' for v, t in d_type.enum_values.items()], 1)) elif is_primitive_python_type(d_type.python_type): lines.append(f'{d_type.name} = {d_type.python_type}') lines.extend(docstring_lines) @@ -109,7 +109,7 @@ def _object_field(field: ObjectField) -> List[str]: return lines -def data_types(d_types: List[DataType]) -> List[str]: +def data_types(d_types: List[DataType], default_package: str) -> List[str]: already_defined = [kw for kw in keyword.kwlist] already_defined += ['int', 'float', 'complex', 'str', 'list', 'tuple', 'range', 'bytes', 'bytearray', 'memoryview', 'dict', @@ -142,6 +142,7 @@ def data_types(d_types: List[DataType]) -> List[str]: # Declare types in dependency order total_defined = 0 n_defined = 1 + not_defined = [] def _core_type(type_name): core_type = type_name @@ -167,10 +168,39 @@ def _core_type(type_name): 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] + + not_defined = [d_type for d_type in d_types if + d_type.name not in already_defined] + if not not_defined: + break + + # Declare certain types external + if n_defined == 0: + remaining_names = {d_type.name for d_type in d_types + if d_type.name not in already_defined} + for d_type in d_types: + only_external_undefined_fields = True + for f in d_type.fields: + core_field_type = _core_type(f.python_type) + if core_field_type not in already_defined and core_field_type in remaining_names: + only_external_undefined_fields = False + break + if only_external_undefined_fields: + for f in d_type.fields: + core_field_type = _core_type(f.python_type) + if core_field_type not in already_defined: + lines.extend(['', '']) + lines.append(f'from {default_package} import {core_field_type}') + already_defined.append(core_field_type) + n_defined += 1 + break + if not_defined: - not_defined_list = ', '.join(d_type.name for d_type in not_defined) + not_defined_list = '; '.join( + [t.name + ' (' + ', '.join(_core_type(f.python_type) + for f in t.fields + if _core_type(f.python_type) not in already_defined) + ')' + for t in not_defined]) raise RuntimeError(f'Failed to define data types: {not_defined_list}') lines.append('')