diff --git a/docs/graphql.md b/docs/graphql.md index 2735c9a2..3b258dbb 100644 --- a/docs/graphql.md +++ b/docs/graphql.md @@ -1,73 +1,361 @@ # Vspec Graphql Exporter -This exporter allows to automatically generate a graphql schema that can represent VSS data. -The resulting schema does only allow querying information. Mutations are not supported. +This exporter generates a valid GraphQL schema out of the VSS specification. +The schema is constructed according to the [GraphQL Schema Language](https://graphql.org/learn/schema/). -The resulting schema will look something like this: +## VSS to GraphQL Mapping +### VSS elements in a nutshell +VSS resembles a hierarchical tree, where a concept of interest is a node in it. +The actual data point appears always as a `leaf` in the tree (i.e., one of type `attribute`, `sensor`, or `actuator`). +The context for those `leaf` nodes is implicitly captured by grouping them under particular `branch` nodes. +For example, in the following diagram, the window position results in the name `Vehicle.Cabin.Door.Row1.DriverSide.Window.Position`. Here, `Row1.DriverSide` refers to a particular instance of the concept `Door`. +```mermaid +graph TD + Vehicle --> Cabin + Vehicle --> OtherBranch + Cabin --> Door + Door --> Row1 + Row1 --> DriverSide + DriverSide --> Window + Door --> OtherInstance + Window --> Position + Window --> OtherLeaf + OtherBranch --> SomeLeaf + classDef white fill:#FFFFFF,stroke:#000000,stroke-width:2px,color:#000000; + + classDef gray fill:#F5F5F5,stroke:#666666,stroke-width:2px,color:#000000; + + classDef blue fill:#DAE8FC,stroke:#6C8EBF,stroke-width:2px,color:#000000; + + classDef green fill:#D5E8D4,stroke:#82B366,stroke-width:2px,color:#000000; + + classDef orange fill:#FFE6CC,stroke:#D79B00,stroke-width:2px,color:#000000; + + classDef yellow fill:#FFF2CC,stroke:#D6B656,stroke-width:2px,color:#000000; + + classDef red fill:#F8CECC,stroke:#B85450,stroke-width:2px,color:#000000; + + classDef purple fill:#E1D5E7,stroke:#9673A6,stroke-width:2px,color:#000000; + + class Vehicle,Cabin,Door,Window,OtherBranch blue; + class Row1,DriverSide,OtherInstance yellow; + class Position,OtherLeaf,SomeLeaf red; +``` + +### VSS metadata +The following table shows the possible metadata for VSS elements: +| VSS metadata item | Description | Mandatory? | In `branch`? | In `leaf`? | +|--------------|--------------------------------------------|------------|--------------|------------| +| `fqn` | Fully qualified name of the element (aka., VSS `Signal`) | Yes | Yes | Yes | +| `type` | One of `branch`, `attribute`, `sensor`, or `actuator` | Yes | Yes | Yes | +| `description`| Description of the element | Yes | Yes | Yes | +| `comment` | Additional comments about the element | No | Yes | Yes | +| `deprecation`| Deprecation status of the element | No | Yes | Yes | +| `instances` | Labels identifying multiple occurrences | No | Yes | No | +| `datatype` | Data type of the leaf element | Yes | No | Yes | +| `unit` | Unit of measurement for the leaf element | No | No | Yes | +| `min` | Minimum value for the leaf element | No | No | Yes | +| `max` | Maximum value for the leaf element | No | No | Yes | +| `allowed` | Allowed values for the leaf element | No | No | Yes | +| `default` | Default value for the leaf element | No | No | Yes | + + +### Mapping rules for VSS `branch` nodes +* `fqn` + * The `fqn` is used to construct the name of a GraphQL `type`. +* `type: branch` + * We distiguish a pure VSS `branch` from those that are `instances`. The aim is to focus the modeling process only on the abstract concepts that capture the intended meaning without repetition of entries. + * Only if the `branch` is not an instance of another branch, a `GraphQL type` whith name based on the `fqn` is created. +* `description` + * A doc string above the GraphQL `type` definition. +* `comment` + * Included in the doc string with the prefix `@comment`. +* `deprecation` + * Using the GraphQL `@deprecated` directive. +* `instances` + * An `instance:` GraphQL `field` is created inside the `type` definition. This field points to a particular `enum`. + * A GraphQL `enum` named as `InstanceEnum` is created with the values specified in the vspec. + * The parent node to which the instantiatable branch belongs will contain a field for that in the **plural** form. Also, the assotiated type for the value is created as an array to indicate the possibility of having multiple instances of such an element. For example, if the type `Cabin` is expected to have multiple doors, then a field `doors: [Door]` will appear there. + +#### Example in vspec +For example, considering the concepts `Vehicle`, `Cabin`, `Door`, and `Window`. +They are specified in `vspec` as follows: +```yaml +Vehicle: + description: High-level vehicle data. + type: branch + +Vehicle.Cabin: + description: All in-cabin components, including doors. + type: branch + +Vehicle.Cabin.Door: + type: branch + instances: + - Row[1,2] + - ["DriverSide","PassengerSide"] + description: All doors, including windows and switches. + +Vehicle.Cabin.Door.Window: + type: branch + description: Door window status. Start position for Window is Closed. +``` +If we directly expand the tree, it will result in unnecesary repetition of the specification for the instantiated branches. + +```mermaid +graph TD + Vehicle --> Cabin + Cabin --> Door + Door --> Row1.DriverSide + Door --> Row2.DriverSide + Door --> Row1.PassengerSide + Door --> Row2.PassengerSide + + Row1.PassengerSide --> Row1.PassengerSide.Window + Row2.PassengerSide --> Row2.PassengerSide.Window + Row1.DriverSide --> Row1.DriverSide.Window + Row2.DriverSide --> Row2.DriverSide.Window + + + classDef blue fill:#DAE8FC,stroke:#6C8EBF,stroke-width:2px,color:#000000; + + classDef yellow fill:#FFF2CC,stroke:#D6B656,stroke-width:2px,color:#000000; + + class Vehicle,Cabin,Door,Row1.PassengerSide.Window,Row2.PassengerSide.Window,Row1.DriverSide.Window,Row2.DriverSide.Window blue; + class Row1.PassengerSide,Row2.PassengerSide,Row1.DriverSide,Row2.DriverSide yellow; +``` + +
+ +Click here to expand an example YAML export + +#### Example of repeated concepts in the yaml export. + +```yaml +Vehicle: + description: High-level vehicle data. + type: branch + +Vehicle.Cabin: + description: All in-cabin components, including doors. + type: branch + +Vehicle.Cabin.Door: + description: All doors, including windows and switches. + type: branch + +Vehicle.Cabin.Door.Row1: + description: All doors, including windows and switches. + type: branch + +Vehicle.Cabin.Door.Row1.DriverSide: + description: All doors, including windows and switches. + type: branch + +Vehicle.Cabin.Door.Row1.DriverSide.Window: + description: Door window status. Start position for Window is Closed. + type: branch + +Vehicle.Cabin.Door.Row1: + description: All doors, including windows and switches. + type: branch + +Vehicle.Cabin.Door.Row1.DriverSide: + description: All doors, including windows and switches. + type: branch + +Vehicle.Cabin.Door.Row1.DriverSide.Window: + description: Door window status. Start position for Window is Closed. + type: branch + +Vehicle.Cabin.Door.Row2: + description: All doors, including windows and switches. + type: branch + +Vehicle.Cabin.Door.Row2.DriverSide: + description: All doors, including windows and switches. + type: branch + +Vehicle.Cabin.Door.Row2.DriverSide.Window: + description: Door window status. Start position for Window is Closed. + type: branch +``` + +
+ + +#### Example in GraphQL. + + +To convey the same information in a compact way, the GraphQL correspondance would be: ```graphql -type Query { - vehicle( - """VIN of the vehicle that you want to request data for.""" - id: String! - - """ - Filter data to only provide information that was sent from the vehicle after that timestamp. - """ - after: String - ): Vehicle +"""All in-cabin components, including doors.""" +type Cabin { + doors: [Door] } -"""Highlevel vehicle data.""" -type Vehicle { - """Attributes that identify a vehicle""" - vehicleIdentification: Vehicle_VehicleIdentification - ... +"""All doors, including windows and switches.""" +type Door { + instanceLabel: doorInstanceEnum + window: Window } -type Vehicle_VehicleIdentification { - ... +"""Door window status. Start position for Window is Closed.""" +type Window { + ... } -... -``` - -Leaves look like this: -```graphql -"""Vehicle brand or manufacturer""" -type Vehicle_VehicleIdentification_Brand { - """Value: Vehicle brand or manufacturer""" - value: String - """Timestamp: Vehicle brand or manufacturer""" - timestamp: String +"""Set of possible values for the instance name of a Door.""" +type doorInstanceEnum { + ROW1_DRIVERSIDE + ROW1_PASSENGERSIDE + ROW2_DRIVERSIDE + ROW2_PASSENGERSIDE } ``` -Every leaf has a timestamp. This is supposed to contain the date of the last modification of the value. -Queries can then filter data that has been recorded after a given timestamp. +### Mapping rules for VSS `leaf` nodes +* `fqn` + * The last part of the `fqn` (i.e., the node's name itself) becomes the name of a GraphQL `field` inside a particular GraphQL `type`. +* `type` + * Since GraphQL specifies a contract between the data producer and data consumer. The specified data can be made readable (via `Query`) and/or writtable (via `Mutation`). + * Optional: Regardless of the `VSS type`, every leaf can have a field in a `Query` to indicate that the value can be read. This is listed as optional because some concepts might not be desired to be queried atomically. + * Optional: If `VSS type` is `actuator`, a `Mutation` for that concept could be specified. This is listed as optional because some concepts might not be desired to be modifiable atomically. +* `description` + * A doc string above the `GraphQL field` definition. +* `comment` + * Included in the doc string with the prefix `@comment`. +* `deprecation` + * Using the GraphQL `@deprecated` directive. +* `datatype` + * Using the built-in `scalar`. + * Custom `scalar` are also provided to cover other datatypes. +* `unit` + * Used as an attribute in a particular `GraphQL field` +* `min` + * Added as info to the doc string `@min:`. +* `max` + * Added as info to the doc string `@max:`. +* `allowed` + * A `GraphQL Enum` is created to hold the set of possible values expected for that `GraphQL Field` +* `default` + * Added as info to the doc string as `@default:`. -### Additional leaf parameters -As for `timestamp` in some scenarios it makes sense to add certain metadata like the `source` of a -served signal or additional privacy information. Therefore the tool has an additional calling parameter -`--gqlfield `, which takes the name and description of the additional field, like: +#### Example in vspec +Considering the `Position` and `Switch` properties of the `Row1.DriverSide.Window`, its specification looks like: +```yaml +Vehicle.Cabin.Door.Row1.DriverSide.Window.Position: + comment: Relationship between Open/Close and Start/End position is item dependent. + datatype: uint8 + description: Item position. 0 = Start position 100 = End position. + max: 100 + min: 0 + type: actuator + unit: percent -```bash ---gqlfield "source" "Source System" +Vehicle.Cabin.Door.Row1.DriverSide.Window.Switch: + allowed: + - INACTIVE + - CLOSE + - OPEN + - ONE_SHOT_CLOSE + - ONE_SHOT_OPEN + datatype: string + description: Switch controlling sliding action such as window, sunroof, or blind. + type: actuator ``` -Resulting in the following leaf in the schema: - +#### Example in GraphQL ```graphql -"""Vehicle brand or manufacturer""" -type Vehicle_VehicleIdentification_Brand { - """Value: Vehicle brand or manufacturer.""" - value: String +"""Door window status. Start position for Window is Closed.""" +type Window { + """ + Item position. 0 = Start position 100 = End position. + @comment: Relationship between Open/Close and Start/End position is item dependent. + @min: 0 + @max: 100 + """ + position(unit: RelationUnit = PERCENT): UInt8 - """Timestamp: Vehicle brand or manufacturer.""" - timestamp: String + """ + Switch controlling sliding action such as window, sunroof, or blind. + """ + switch: WindowSwitchEnum +} - """ Source System: Vehicle brand or manufacturer.""" - source: String +enum WindowSwitchEnum { + INACTIVE + CLOSE + OPEN + ONE_SHOT_CLOSE + ONE_SHOT_OPEN } ``` + +### Mapping references +The exporter has the option to save the mapping references to a `.json` file by using the `--legacy-mapping-output`. For example: +```shell +vspec export graphql --vspec path_to_spec.vspec --output path_to_output_schema.graphql --legacy-mapping-output path_to_mapping_file.json +``` + +The mapping reference file will look like follows: +```json +{ + "quantity_kinds_and_units": { + "info": "Mappings of vspec quantity kind and their units to the corresponding names in GraphQL.", + "mappings": { + "acceleration": { + "gql_unit_enum": "AccelerationUnit_Enum", + "units": { + "centimeters per second squared": "CENTIMETERS_PER_SECOND_SQUARED", + "meters per second squared": "METERS_PER_SECOND_SQUARED" + } + }, + ... + }, + }, + + "vspec_branches": { + "info": "Mappings of vspec branches to the corresponding names in GraphQL.", + "mappings": { + "Vehicle.Body.Lights.Beam": { + "gql_type": "Vehicle_Body_Lights_Beam", + "gql_instance_enum": "Vehicle_Body_Lights_Beam_Instance_Enum", + "instance_labels": { + "Low": "LOW", + "High": "HIGH" + } + }, + ... + }, + }, + + "vspec_leaves": { + "info": "Mappings of vspec leaves to the corresponding names in GraphQL.", + "mappings": { + "Vehicle.ADAS.ActiveAutonomyLevel": { + "gql_field": "activeAutonomyLevel", + "in_gql_type": "Vehicle_ADAS", + "gql_allowed_enum": "Vehicle_ADAS_ActiveAutonomyLevel_Enum", + "allowed_values": { + "SAE_0": "SAE_0", + "SAE_1": "SAE_1", + "SAE_2_DISENGAGING": "SAE_2_DISENGAGING", + "SAE_2": "SAE_2", + "SAE_3_DISENGAGING": "SAE_3_DISENGAGING", + "SAE_3": "SAE_3", + "SAE_4_DISENGAGING": "SAE_4_DISENGAGING", + "SAE_4": "SAE_4", + "SAE_5_DISENGAGING": "SAE_5_DISENGAGING", + "SAE_5": "SAE_5" + } + }, + ... + } + + + + +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f1fa2258..da1fe597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ dependencies = [ "rich-click>=1.8.3", "rich>=13.9.4", "ruff>=0.7.3", + "graphene>=3.4.3", + "pandas>=2.2.3", ] authors = [ {name="COVESA VSS", email="covesa-dev@covesa.global"} diff --git a/src/vss_tools/datatypes.py b/src/vss_tools/datatypes.py index 22eb1cd7..9701d3b6 100644 --- a/src/vss_tools/datatypes.py +++ b/src/vss_tools/datatypes.py @@ -5,15 +5,21 @@ # https://www.mozilla.org/en-US/MPL/2.0/ # # SPDX-License-Identifier: MPL-2.0 -from typing import Any, Callable, Set +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, Set from vss_tools import log +if TYPE_CHECKING: + from vss_tools.model import VSSUnit + # Global objects to be extended by other code parts dynamic_datatypes: Set[str] = set() dynamic_struct_schemas: dict[str, dict[str, Any]] = {} dynamic_quantities: list[str] = [] -dynamic_units: dict[str, list] = {} # unit name -> allowed datatypes +# Map of unit name and VSSUnit +dynamic_units: dict[str, VSSUnit] = {} class DatatypesException(Exception): diff --git a/src/vss_tools/exporters/graphql.py b/src/vss_tools/exporters/graphql.py index e7a0d342..0e62c07f 100644 --- a/src/vss_tools/exporters/graphql.py +++ b/src/vss_tools/exporters/graphql.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Contributors to COVESA +# Copyright (c) 2024 Contributors to COVESA # # This program and the accompanying materials are made available under the # terms of the Mozilla Public License 2.0 which is available at @@ -6,129 +6,514 @@ # # SPDX-License-Identifier: MPL-2.0 + +from __future__ import annotations + +import json +import keyword +import re +import sys +from enum import Enum +from itertools import product from pathlib import Path -from typing import Dict +from typing import Any, Dict +import graphene +import pandas as pd import rich_click as click -from graphql import ( - GraphQLArgument, - GraphQLBoolean, - GraphQLField, - GraphQLFloat, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLSchema, - GraphQLString, - print_schema, -) +from anytree import PreOrderIter +from graphene import Field, Scalar import vss_tools.cli_options as clo from vss_tools import log -from vss_tools.datatypes import Datatypes +from vss_tools.datatypes import Datatypes, dynamic_units, is_array from vss_tools.main import get_trees -from vss_tools.model import VSSDataDatatype -from vss_tools.tree import VSSNode -from vss_tools.utils.misc import camel_back - -GRAPHQL_TYPE_MAPPING = { - Datatypes.INT8[0]: GraphQLInt, - Datatypes.INT8_ARRAY[0]: GraphQLList(GraphQLInt), - Datatypes.UINT8[0]: GraphQLInt, - Datatypes.UINT8_ARRAY[0]: GraphQLList(GraphQLInt), - Datatypes.INT16[0]: GraphQLInt, - Datatypes.INT16_ARRAY[0]: GraphQLList(GraphQLInt), - Datatypes.UINT16[0]: GraphQLInt, - Datatypes.UINT16_ARRAY[0]: GraphQLList(GraphQLInt), - Datatypes.INT32[0]: GraphQLInt, - Datatypes.INT32_ARRAY[0]: GraphQLList(GraphQLInt), - Datatypes.UINT32[0]: GraphQLFloat, - Datatypes.UINT32_ARRAY[0]: GraphQLList(GraphQLFloat), - Datatypes.INT64[0]: GraphQLFloat, - Datatypes.INT64_ARRAY[0]: GraphQLList(GraphQLFloat), - Datatypes.UINT64[0]: GraphQLFloat, - Datatypes.UINT64_ARRAY[0]: GraphQLList(GraphQLFloat), - Datatypes.FLOAT[0]: GraphQLFloat, - Datatypes.FLOAT_ARRAY[0]: GraphQLList(GraphQLFloat), - Datatypes.DOUBLE[0]: GraphQLFloat, - Datatypes.DOUBLE_ARRAY[0]: GraphQLList(GraphQLFloat), - Datatypes.BOOLEAN[0]: GraphQLBoolean, - Datatypes.BOOLEAN_ARRAY[0]: GraphQLList(GraphQLBoolean), - Datatypes.STRING[0]: GraphQLString, - Datatypes.STRING_ARRAY[0]: GraphQLList(GraphQLString), -} +from vss_tools.tree import VSSNode, expand_string, get_expected_parent +from vss_tools.utils.misc import getattr_nn, str_to_screaming_snake_case +from .samm.helpers.string_helper import str_to_lc_first_camel_case, str_to_uc_first + + +class GraphQLExporterException(Exception): + """Exception raised for errors in the GraphQL export process.""" -class GraphQLFieldException(Exception): pass -def get_schema_from_tree(root: VSSNode, additional_leaf_fields: list) -> str: - """Takes a VSSNode and additional fields for the leafs. Returns a graphql schema as string.""" - args = dict( - id=GraphQLArgument( - GraphQLNonNull(GraphQLString), - description="VIN of the vehicle that you want to request data for.", - ), - after=GraphQLArgument( - GraphQLString, - description=( - "Filter data to only provide information that was sent " "from the vehicle after that timestamp." - ), - ), - ) +# ========= Custom GraphQL scalar types ========= +class Int8(Scalar): + pass - root_query = GraphQLObjectType( - "Query", - lambda: {"vehicle": GraphQLField(to_gql_type(root, additional_leaf_fields), args)}, - ) - return print_schema(GraphQLSchema(root_query)) + +class UInt8(Scalar): + pass + + +class Int16(Scalar): + pass + + +class UInt16(Scalar): + pass + + +class UInt32(Scalar): + pass + + +class Int64(Scalar): + pass + + +class UInt64(Scalar): + pass + + +# ========= Mapping aids ========= +class GQLElementType(Enum): + """Enum of GraphQL elements to better handle naming conventions during the export process""" + + TYPE = "type" + FIELD = "field" + ARGUMENT = "argument" + DIRECTIVE = "directive" + ENUM = "enum" + INTERFACE = "interface" + UNION = "union" + SCALAR = "scalar" + ENUM_VALUE = "enum_value" + + +datatype_map = { + Datatypes.INT8[0]: Int8, + Datatypes.INT8_ARRAY[0]: graphene.List(Int8), + Datatypes.UINT8[0]: UInt8, + Datatypes.UINT8_ARRAY[0]: graphene.List(UInt8), + Datatypes.INT16[0]: Int16, + Datatypes.INT16_ARRAY[0]: graphene.List(Int16), + Datatypes.UINT16[0]: UInt16, + Datatypes.UINT16_ARRAY[0]: graphene.List(UInt16), + Datatypes.INT32[0]: graphene.Int, + Datatypes.INT32_ARRAY[0]: graphene.List(graphene.Int), + Datatypes.UINT32[0]: UInt32, + Datatypes.UINT32_ARRAY[0]: graphene.List(UInt32), + Datatypes.INT64[0]: Int64, + Datatypes.INT64_ARRAY[0]: graphene.List(Int64), + Datatypes.UINT64[0]: UInt64, + Datatypes.UINT64_ARRAY[0]: graphene.List(UInt64), + Datatypes.FLOAT[0]: graphene.Float, + Datatypes.FLOAT_ARRAY[0]: graphene.List(graphene.Float), + Datatypes.DOUBLE[0]: graphene.Float, + Datatypes.DOUBLE_ARRAY[0]: graphene.List(graphene.Float), + Datatypes.BOOLEAN[0]: graphene.Boolean, + Datatypes.BOOLEAN_ARRAY[0]: graphene.List(graphene.Boolean), + Datatypes.STRING[0]: graphene.String, + Datatypes.STRING_ARRAY[0]: graphene.List(graphene.String), +} + +# ========= Global variables ========= +vss_branches_df = pd.DataFrame() +vss_leaves_df = pd.DataFrame() +gql_allowed_enums: Dict[str, graphene.Enum] = {} +gql_instance_enums: Dict[str, graphene.Enum] = {} +gql_unit_enums: Dict[str, graphene.Enum] = {} + +mapping_quantity_kinds_df = pd.DataFrame(columns=["vspec_quantity_kind", "gql_unit_enum", "units"]).set_index( + "vspec_quantity_kind" +) +mapping_branches_df = pd.DataFrame(columns=["vspec_fqn", "gql_type", "gql_instance_enum", "instance_labels"]).set_index( + "vspec_fqn" +) +mapping_leaves_df = pd.DataFrame( + columns=["vspec_fqn", "gql_field", "in_gql_type", "gql_allowed_enum", "allowed_values"] +).set_index("vspec_fqn") + + +def get_gql_name(text: str, gql_type: GQLElementType) -> str: + """ + Converts a given text to a GraphQL compatible name based on the given GQLElementType. + For example: + - type, enum, and scalars must be PascalCase. + - fields and arguments must be camelCase. + - enum values must be SCREAMING_SNAKE_CASE. + """ + + if gql_type in {GQLElementType.FIELD, GQLElementType.ARGUMENT, GQLElementType.DIRECTIVE}: + text = str_to_lc_first_camel_case(text.replace(".", "_")) + return text if not keyword.iskeyword(text) else f"{text}_" + elif gql_type in { + GQLElementType.TYPE, + GQLElementType.ENUM, + GQLElementType.INTERFACE, + GQLElementType.UNION, + GQLElementType.SCALAR, + }: + text = text.replace(".", "_").replace("-", "") + text = f"{text}_Enum" if gql_type == GQLElementType.ENUM else text + text = str_to_uc_first(text) + return text if not keyword.iskeyword(text) else f"{text}_" + elif gql_type in {GQLElementType.ENUM_VALUE}: + text = str_to_screaming_snake_case(text) + if text[0].isdigit(): + text = f"_{text}" + return text if not keyword.iskeyword(text) else f"{text}_" + raise GraphQLExporterException(f"Invalid GQLElementType: {gql_type}") + + +def get_unit_enum_name(quantity_kind: str) -> str: + """Get the name for the GraphQL unit enum""" + text = f"{quantity_kind}_Unit" + return get_gql_name(str_to_uc_first(text), GQLElementType.ENUM) + + +# ========= DataFrames for VSS metadata ========= +core_headers = ["fqn", "parent", "name", "type", "description", "comment", "deprecated"] +leaf_specific_headers = ["datatype", "unit", "min", "max", "allowed", "default"] +branch_specific_headers = ["instances"] + + +def get_metadata_df(root: VSSNode) -> pd.DataFrame: + """Traverses the tree and returns a DataFrame with the metadata of all the nodes.""" + headers = core_headers + leaf_specific_headers + branch_specific_headers + + df = pd.DataFrame(columns=headers) + + for node in PreOrderIter(root): + data = node.get_vss_data() + fqn = node.get_fqn() + parent = get_expected_parent(fqn) + name = node.name + vss_type = data.type.value + metadata = {} + for header in headers[2:]: + metadata[header] = getattr_nn(data, header, "") + metadata["fqn"] = fqn + metadata["parent"] = parent + metadata["name"] = name + metadata["type"] = vss_type + df = pd.concat([df, pd.DataFrame([metadata])], ignore_index=True) + + return df.sort_values(by="fqn") + + +def get_vss_branches_df(df: pd.DataFrame) -> pd.DataFrame: + """Returns a DataFrame with the metadata of all the branches.""" + branch_headers = core_headers + branch_specific_headers + df = df[df["type"].isin(["branch"])] + return df[branch_headers].set_index("fqn") + + +def get_vss_leaves_df(df: pd.DataFrame) -> pd.DataFrame: + """Returns a DataFrame with the metadata of all the leaves.""" + leaf_headers = core_headers + leaf_specific_headers + df = df[df["type"].isin(["attribute", "sensor", "actuator"])] + return df[leaf_headers].set_index("fqn") + + +def get_gql_unit_enums() -> Dict[str, graphene.Enum]: + """Get GraphQL enums for VSS units and quantity kinds.""" + global mapping_quantity_kinds_df + + spec_quantity_kinds = get_quantity_kinds_and_units() + unit_enums: Dict[str, graphene.Enum] = {} + + # Create a graphene enum for each key in the spec_quantity_kinds + for quantity_kind, units in spec_quantity_kinds.items(): + enum_name = get_unit_enum_name(quantity_kind) + enum_values = {} + unit_mappings = {} + for unit in units: + unit_name = get_gql_name(unit, GQLElementType.ENUM_VALUE) + unit_mappings[unit] = unit_name + enum_values[unit_name] = unit_name + + unit_enums[enum_name] = type(enum_name, (graphene.Enum,), enum_values) # type: ignore + mapping_quantity_kinds_df.loc[quantity_kind] = [enum_name, sort_dict_by_key(unit_mappings)] + + return unit_enums + + +def get_quantity_kinds_and_units() -> dict[str, set[str]]: + """Get the quantity kinds and their units as specified in VSS.""" + spec_quantity_kinds: Dict[str, set[str]] = {} + for unit_data in dynamic_units.values(): + quantity_kind_name = unit_data.quantity + if unit_data.unit: + unit_name = unit_data.unit + if quantity_kind_name not in spec_quantity_kinds: + spec_quantity_kinds[quantity_kind_name] = set() + spec_quantity_kinds[quantity_kind_name].add(unit_name) + + return dict(sorted(spec_quantity_kinds.items())) -def to_gql_type(node: VSSNode, additional_leaf_fields: list) -> GraphQLObjectType: - if isinstance(node.data, VSSDataDatatype): - fields = leaf_fields(node, additional_leaf_fields) +def get_branches_with_specified_instances() -> pd.DataFrame: + """Get the branches that have instances specified.""" + return vss_branches_df[vss_branches_df["instances"].astype(str) != "[]"] + + +def get_instances_enums() -> Dict[str, graphene.Enum]: + """Create a GraphQL enum for each branch that has instances specified.""" + enums: Dict[str, graphene.Enum] = {} + branches_with_instances = get_branches_with_specified_instances() + + for fqn, row in branches_with_instances.iterrows(): + spec_instances = row["instances"] + instance_labels = expand_instance_labels(spec_instances) + mapping_instance_labels = {} + + enum_name = get_gql_name(f"{fqn}.Instance", GQLElementType.ENUM) # TODO: Todo pass a shorter name! + enum_values = {} + + for label in instance_labels: + value = get_gql_name(label, GQLElementType.ENUM_VALUE) + enum_values[value] = value + mapping_instance_labels[label] = value + + enums[fqn] = type(enum_name, (graphene.Enum,), enum_values) # type: ignore + + global mapping_branches_df + mapping_branches_df.loc[fqn, ["gql_instance_enum", "instance_labels"]] = [enum_name, mapping_instance_labels] # type: ignore + + return enums + + +def expand_instance_labels(instances: list[str]) -> list[str]: + """ + Expands the instance expressions into a unidimensional list of combined labels. + + Example 1: + - input: ['Row[1,2]', ['DriverSide', 'PassengerSide']] + - result: ['Row1.DriverSide', 'Row1.PassengerSide', 'Row2.DriverSide', 'Row2.PassengerSide'] + + Example 2: + - input: ['Rear', 'Front'] + - result: ['Rear', 'Front'] + """ + + pattern = r".*\[\d+,\d+\].*" + if not is_list_inside(instances) and not any(re.match(pattern, i) for i in instances): + return instances # If the instance is not a list and does not contain a range, return it as is. else: - fields = branch_fields(node, additional_leaf_fields) - return GraphQLObjectType( - name=node.get_fqn("_"), - fields=fields, - description=node.get_vss_data().description, - ) + instance_levels = [] + for instance_expression in instances: + if isinstance(instance_expression, str) and re.match(pattern, instance_expression): + instance_levels.append(expand_string(instance_expression)) + elif isinstance(instance_expression, list): + instance_levels.append(instance_expression) + else: + log.warning(f"Special case not considered: {instance_expression}") + + expanded_labels = [] + + if is_list_inside(instance_levels): + expanded_labels = [".".join(item) for item in product(*instance_levels)] + else: + expanded_labels = instance_levels + + return expanded_labels + + +def is_list_inside(some_list: list[str]) -> bool: + """Check if a list contains another list.""" + return any(isinstance(element, list) for element in some_list) + + +def get_allowed_enums() -> Dict[str, graphene.Enum]: + """Create a GraphQL enum for each leaf that has allowed values specified.""" + gql_allowed_enums: Dict[str, graphene.Enum] = {} + + leaves_with_allowed = vss_leaves_df[vss_leaves_df["allowed"].astype(str) != ""] + + for fqn, row in leaves_with_allowed.iterrows(): + allowed_list = eval(row["allowed"]) if isinstance(row["allowed"], str) else row["allowed"] + enum_values = {} + mapping_allowed_values = {} + for allowed_value in allowed_list: + enum_name = get_gql_name(str(fqn), GQLElementType.ENUM) + value = get_gql_name(str(allowed_value), GQLElementType.ENUM_VALUE) + enum_values[value] = value + mapping_allowed_values[allowed_value] = value + + gql_allowed_enums[fqn] = type(enum_name, (graphene.Enum,), enum_values) # type: ignore + global mapping_leaves_df + mapping_leaves_df.loc[fqn, ["gql_allowed_enum", "allowed_values"]] = [enum_name, mapping_allowed_values] # type: ignore + + return gql_allowed_enums + + +def get_gql_object_types() -> Dict[str, graphene.ObjectType]: + """Create a GraphQL object type for each branch in the VSS.""" + gql_object_types: Dict[str, graphene.ObjectType] = {} + + for fqn, _ in vss_branches_df.iterrows(): + gql_object_types[str(fqn)] = create_gql_object_type(str(fqn)) + + return gql_object_types -def leaf_fields(node: VSSNode, additional_leaf_fields: list) -> Dict[str, GraphQLField]: - field_dict = {} - datatype = getattr(node.data, "datatype", None) - if datatype is not None: - field_dict["value"] = field(node, "Value: ", GRAPHQL_TYPE_MAPPING[datatype]) - field_dict["timestamp"] = field(node, "Timestamp: ") - if additional_leaf_fields: - for additional_field in additional_leaf_fields: - if len(additional_field) == 2: - field_dict[additional_field[0]] = field(node, f" {str(additional_field[1])}: ") +def get_description(fqn: str) -> str: + description = "" + if fqn in vss_branches_df.index: + description = str(vss_branches_df.loc[fqn, "description"]) + elif fqn in vss_leaves_df.index: + description = str(vss_leaves_df.loc[fqn, "description"]) + if vss_leaves_df.loc[fqn, "min"]: + description += f"\n@min: {str(vss_leaves_df.loc[fqn, 'min'])}" + if vss_leaves_df.loc[fqn, "max"]: + description += f"\n@max: {str(vss_leaves_df.loc[fqn, 'max'])}" + if vss_leaves_df.loc[fqn, "default"]: + description += f"\n@default: {str(vss_leaves_df.loc[fqn, 'default'])}" + return description + + +def create_gql_object_type(fqn: str) -> graphene.ObjectType: + """Create a GraphQL object type for a given Fully Qualified Name (fqn) fqn the VSS.""" + # Select the children of the current branch + child_leaves = vss_leaves_df[vss_leaves_df["parent"] == fqn] + child_branches = vss_branches_df[vss_branches_df["parent"] == fqn] + branches_with_instances = get_branches_with_specified_instances() + + gql_fields: Dict[str, graphene.Field] = {} + gql_type_name = get_gql_name(fqn, GQLElementType.TYPE) + gql_type_description = get_description(fqn) + + if fqn in branches_with_instances.index: + gql_fields["instance"] = Field(name="instance", type_=gql_instance_enums[fqn]) + + # Create a GraphQL field for each leaf that belongs to the current branch + for child_fqn, child_leaf_metadata_row in child_leaves.iterrows(): + field_name = get_gql_name(child_leaf_metadata_row["name"], GQLElementType.FIELD) + field_type = None + field_description = get_description(str(child_fqn)) + unit = child_leaf_metadata_row["unit"] + allowed = child_leaf_metadata_row["allowed"] + extra_args = {} + + if allowed == "": + # No allowed values are specified, then use the scalars from datatype_map as the field type + field_type = datatype_map[child_leaf_metadata_row["datatype"]] + + else: + # Allowed values are specified + allowed_enum = gql_allowed_enums[str(child_fqn)] + + datatype = child_leaf_metadata_row["datatype"] + if datatype and is_array(datatype): + field_type = graphene.List(allowed_enum) else: - raise GraphQLFieldException("", "", "Incorrect format of graphql field specification") - unit = getattr(node.data, "unit", None) - if unit: - field_dict["unit"] = field(node, "Unit of ") - return field_dict + field_type = allowed_enum + + if unit: + try: + quantity_kind = dynamic_units[unit].quantity # Quantity kind name as described in VSS + enum_name = get_unit_enum_name(quantity_kind) # GraphQL enum name for the quantity kind + unit_enum = gql_unit_enums[enum_name] + unit_value = dynamic_units[unit].unit + + if unit_value is not None: + unit_enum_value = get_gql_name(unit_value, GQLElementType.ENUM_VALUE) + else: + raise GraphQLExporterException(f"Unit value for '{unit}' is None") + + if unit_enum_value not in unit_enum._meta.enum.__members__: + raise GraphQLExporterException( + f"Unit enum value '{unit_enum_value}' not found in enum '{unit_enum}'" + ) + else: + extra_args["unit"] = graphene.Argument(type_=unit_enum, default_value=unit_enum_value) # type: ignore + + gql_fields[field_name] = Field( + name=field_name, type_=field_type, description=field_description, args=extra_args + ) + except GraphQLExporterException as e: + log.error(e) + sys.exit(1) + else: + gql_fields[field_name] = Field(name=field_name, description=field_description, type_=field_type) + + global mapping_leaves_df + mapping_leaves_df.loc[child_fqn, ["gql_field", "in_gql_type"]] = [field_name, gql_type_name] # type: ignore + + # Create a field for each sub branch and call the creation of the GraphQL type recursively + for child_fqn, child_branch_metadata_row in child_branches.iterrows(): + field_name = get_gql_name(child_branch_metadata_row["name"], GQLElementType.FIELD) + field_type = create_gql_object_type(str(child_fqn)) + if child_fqn in branches_with_instances.index: + field_name += "_s" + field_type = graphene.List(field_type) + gql_fields[field_name] = Field(name=field_name, type_=field_type) + + global mapping_branches_df + mapping_branches_df.loc[fqn, "gql_type"] = gql_type_name + return type(gql_type_name, (graphene.ObjectType,), gql_fields, description=gql_type_description) # type: ignore + +def get_graphql_schema(tree: VSSNode) -> graphene.Schema: + """Create a GraphQL schema from the VSS tree.""" + global vss_branches_df, vss_leaves_df, gql_unit_enums, gql_allowed_enums, gql_instance_enums -def branch_fields(node: VSSNode, additional_leaf_fields: list) -> Dict[str, GraphQLField]: - # we only consider nodes that either have children or are datatype leafs - valid = (c for c in node.children if len(c.children) or hasattr(c.data, "datatype")) - return {camel_back(c.name): field(c, type=to_gql_type(c, additional_leaf_fields)) for c in valid} + # TODO: Add flag to enable descriptions in the schema + # Get pandas DataFrame for all the metadata in the vspec + vss_metadata_df = get_metadata_df(tree) -def field(node: VSSNode, description_prefix="", type=GraphQLString) -> GraphQLField: - data = node.get_vss_data() - return GraphQLField( - type, - deprecation_reason=data.deprecation, - description=f"{description_prefix}{data.description}", + # Split the metadata into branches and leaves + vss_branches_df = get_vss_branches_df(vss_metadata_df) + vss_leaves_df = get_vss_leaves_df(vss_metadata_df) + + # Include the custom scalar types even if they are not used by any type in the schema + custom_scalars = [Int8, UInt8, Int16, UInt16, UInt32, Int64, UInt64] + + # Create enums for the instances specified + gql_instance_enums = get_instances_enums() + + # Get GraphQL enums for the units and quantities + gql_unit_enums = get_gql_unit_enums() + + # In the leaves DataFrame, get the entries that have allowed values and create enums for them + gql_allowed_enums = get_allowed_enums() + + # In branches DataFrame, create a GraphQL type for each pure branch (not for instance branches) + gql_branch_types = get_gql_object_types() + + class Query(graphene.ObjectType): + vehicle = graphene.Field(gql_branch_types[get_gql_name(tree.name, gql_type=GQLElementType.TYPE)]) + + # Order the schema as desired + ordered_types = ( + [Query] + + custom_scalars + + list(gql_branch_types.values()) + + list(gql_unit_enums.values()) + + list(gql_instance_enums.values()) + + list(gql_allowed_enums.values()) ) + return graphene.Schema(types=ordered_types, auto_camelcase=False) + + +def export_mappings() -> Dict[str, Dict[str, Any]]: + """Export the mappings of the VSS to the GraphQL schema.""" + mappings = { + "quantity_kinds_and_units": { + "info": "Mappings of vspec quantity kind and their units to the corresponding names in GraphQL.", + "mappings": sort_dict_by_key(mapping_quantity_kinds_df.to_dict(orient="index")), + }, + "vspec_branches": { + "info": "Mappings of vspec branches to the corresponding names in GraphQL.", + "mappings": sort_dict_by_key(mapping_branches_df.fillna("").to_dict(orient="index")), + }, + "vspec_leaves": { + "info": "Mappings of vspec leaves to the corresponding names in GraphQL.", + "mappings": sort_dict_by_key(mapping_leaves_df.fillna("").to_dict(orient="index")), + }, + } + return mappings + + +def sort_dict_by_key(dictionary: dict) -> dict: + """Sorts a dictionary by its keys in a case-insensitive manner but preserves the original key.""" + return dict(sorted(dictionary.items(), key=lambda item: item[0].lower())) @click.command() @@ -142,13 +527,9 @@ def field(node: VSSNode, description_prefix="", type=GraphQLString) -> GraphQLFi @clo.quantities_opt @clo.units_opt @click.option( - "--gql-fields", - "-g", - multiple=True, - help=""" - Add additional fields to the nodes in the graphql schema. - Usage: ','", - """, + "--legacy-mapping-output", + type=click.Path(dir_okay=False, writable=True, path_type=Path), + help="Output json file of the legacy units and quantities", ) def cli( vspec: Path, @@ -160,10 +541,10 @@ def cli( overlays: tuple[Path], quantities: tuple[Path], units: tuple[Path], - gql_fields: list[str], + legacy_mapping_output: Path | None, ): """ - Export as GraphQL. + Export a VSS specification to a GraphQL schema. """ tree, _ = get_trees( vspec=vspec, @@ -174,12 +555,17 @@ def cli( quantities=quantities, units=units, overlays=overlays, + expand=False, ) - log.info("Generating graphql output...") - outfile = open(output, "w") - gqlfields: list[list[str]] = [] - for field in gql_fields: - gqlfields.append(field.split(",")) - outfile.write(get_schema_from_tree(tree, gqlfields)) - outfile.write("\n") - outfile.close() + + log.info("Generating GraphQL output...") + + gql_schema = get_graphql_schema(tree) + mappings = export_mappings() + + with open(output, "w") as outfile: + outfile.write(str(gql_schema)) + + if legacy_mapping_output: + with open(legacy_mapping_output, "w") as mapping_outfile: + mapping_outfile.write(json.dumps(mappings, indent=4)) diff --git a/src/vss_tools/exporters/utils.py b/src/vss_tools/exporters/utils.py new file mode 100644 index 00000000..156ef8b6 --- /dev/null +++ b/src/vss_tools/exporters/utils.py @@ -0,0 +1,107 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +from anytree import PreOrderIter + +from vss_tools.model import VSSDataBranch, VSSDataDatatype +from vss_tools.tree import VSSNode + + +class NoInstanceRootException(Exception): + pass + + +def get_instance_root(root: VSSNode, depth: int = 1) -> tuple[VSSNode, int]: + """ + Getting the root node of a given instance node. + Going the tree upwards + """ + if root.parent is None: + raise NoInstanceRootException() + if isinstance(root.parent.data, VSSDataBranch): + if root.parent.data.is_instance: # Is the inmmediate parent node also a VSS instance branch? + return get_instance_root(root.parent, depth + 1) + else: + return root.parent, depth + else: + raise NoInstanceRootException() + + +def add_children_map_entries(root: VSSNode, fqn: str, replace: str, map: dict[str, str]) -> None: + """ + Adding rename map entries for children of a given node + """ + child: VSSNode + for child in root.children: + child_fqn = child.get_fqn() + new_name = child_fqn.replace(fqn, replace) + map[child_fqn] = new_name + add_children_map_entries(child, fqn, replace, map) + + +def get_instance_mapping(root: VSSNode | None) -> dict[str, str]: + """ + Constructing a rename map of fqn->new_name. + The new name has instances stripped and appending "I" instead + where N is the depth of the instance + """ + if root is None: + return {} + instance_map: dict[str, str] = {} + for node in PreOrderIter(root): + if isinstance(node.data, VSSDataBranch): + if node.data.is_instance: + instance_root, depth = get_instance_root(node) + new_name = instance_root.get_fqn() + "." + "I" + str(depth) + fqn = node.get_fqn() + instance_map[fqn] = new_name + add_children_map_entries(node, fqn, instance_root.get_fqn(), instance_map) + return instance_map + + +def get_instances_meta(root: VSSNode) -> dict[str, list[str]]: + """ + Constructing metadata of instance root nodes fqns and a list of generated nodes + """ + meta: dict[str, list[str]] = {} + for node in PreOrderIter(root, filter_=lambda n: isinstance(n.data, VSSDataBranch) and n.data.is_instance): + if any(c.data.is_instance for c in node.children if isinstance(c.data, VSSDataBranch)): + continue + instance_root, _ = get_instance_root(node) + instance_root_fqn = instance_root.get_fqn() + + instance_name = node.get_fqn().removeprefix(instance_root_fqn + ".") + + if instance_root_fqn in meta: + meta[instance_root_fqn].append(instance_name) + else: + meta[instance_root_fqn] = [instance_name] + return meta + + +def is_VSS_leaf(node: VSSNode) -> bool: + """Check if the node is a VSS leaf (i.e., one of VSS sensor, attribute, or actuator)""" + if isinstance(node.data, VSSDataDatatype): + return True + return False + + +def is_VSS_branch(node: VSSNode) -> bool: + """Check if the node is a VSS branch (and not an instance branch)""" + if isinstance(node.data, VSSDataBranch): + if not node.data.is_instance: + return True + return False + + +def is_VSS_branch_instance(node: VSSNode) -> bool: + """Check if the node is a VSS branch instance)""" + if isinstance(node.data, VSSDataBranch): + if node.data.is_instance: + return True + return False diff --git a/src/vss_tools/main.py b/src/vss_tools/main.py index 355f4960..78fcb8ae 100644 --- a/src/vss_tools/main.py +++ b/src/vss_tools/main.py @@ -69,12 +69,9 @@ def load_quantities_and_units(quantities: tuple[Path, ...], units: tuple[Path, . dynamic_quantities.extend(list(quantity_data.keys())) unit_data = load_units(list(units)) for k, v in unit_data.items(): - allowed_datatypes = [] - if v.allowed_datatypes is not None: - allowed_datatypes = v.allowed_datatypes if v.unit is not None: - dynamic_units[v.unit] = allowed_datatypes - dynamic_units[k] = allowed_datatypes + dynamic_units[v.unit] = v + dynamic_units[k] = v def check_name_violations(root: VSSNode, strict: bool, aborts: tuple[str, ...]) -> None: diff --git a/src/vss_tools/model.py b/src/vss_tools/model.py index 27c681e4..fe760fa9 100644 --- a/src/vss_tools/model.py +++ b/src/vss_tools/model.py @@ -309,9 +309,12 @@ def check_datatype_matching_allowed_unit_datatypes(self) -> Self: referenced in the unit if given """ if self.unit: - assert Datatypes.get_type(self.datatype), f"Cannot use 'unit' with struct datatype: '{self.datatype}'" + assert Datatypes.get_type(self.datatype), f"Cannot use 'unit' with complex datatype: '{self.datatype}'" + allowed_datatypes = dynamic_units[self.unit].allowed_datatypes + if allowed_datatypes is None: + allowed_datatypes = [] assert any( - Datatypes.is_subtype_of(self.datatype.rstrip("[]"), a) for a in dynamic_units[self.unit] + Datatypes.is_subtype_of(self.datatype.rstrip("[]"), a) for a in allowed_datatypes ), f"'{self.datatype}' is not allowed for unit '{self.unit}'" return self diff --git a/src/vss_tools/utils/misc.py b/src/vss_tools/utils/misc.py index 484a8316..c300c1c1 100644 --- a/src/vss_tools/utils/misc.py +++ b/src/vss_tools/utils/misc.py @@ -31,3 +31,10 @@ def camel_back(st): s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", st) s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() return re.sub(r"_([a-z])", lambda x: x.group(1).upper(), s2) + + +def str_to_screaming_snake_case(text: str) -> str: + """Converts a string to screaming snake case (i.e., CAPITAL LETTERS)""" + text = re.sub(r"[^a-zA-Z0-9]", " ", text) + words = text.split() + return "_".join(word.upper() for word in words) diff --git a/tests/vspec/test_allowed/expected.graphql b/tests/vspec/test_allowed/expected.graphql index 97eff20d..37029405 100644 --- a/tests/vspec/test_allowed/expected.graphql +++ b/tests/vspec/test_allowed/expected.graphql @@ -1,62 +1,87 @@ type Query { - vehicle( - """VIN of the vehicle that you want to request data for.""" - id: String! - - """ - Filter data to only provide information that was sent from the vehicle after that timestamp. - """ - after: String - ): A + vehicle: A } +"""8-bit integer""" +scalar Int8 + +"""8-bit unsigned integer""" +scalar UInt8 + +"""16-bit integer""" +scalar Int16 + +"""16-bit unsigned integer""" +scalar UInt16 + +"""32-bit unsigned integer""" +scalar UInt32 + +"""64-bit integer""" +scalar Int64 + +"""64-bit unsigned integer""" +scalar UInt64 + """Branch A.""" type A { + """A float""" + float: A_Float_Enum + + """An int""" + int: A_Int_Enum + """A string""" - string: A_String + string: A_String_Enum """A string array""" - stringArray: A_StringArray + stringArray: [A_StringArray_Enum] +} - """An int""" - int: A_Int +enum AngularSpeedUnit_Enum { + DEGREE_PER_SECOND +} - """A float""" - float: A_Float +enum DistanceUnit_Enum { + METER + MILLIMETER } -"""A string""" -type A_String { - """Value: A string""" - value: String +enum LengthUnit_Enum { + KILOMETER +} - """Timestamp: A string""" - timestamp: String +enum RelationUnit_Enum { + PERCENT } -"""A string array""" -type A_StringArray { - """Value: A string array""" - value: [String] +enum RotationalSpeedUnit_Enum { + REVOLUTIONS_PER_MINUTE +} - """Timestamp: A string array""" - timestamp: String +enum TemperatureUnit_Enum { + DEGREE_CELSIUS } -"""An int""" -type A_Int { - """Value: An int""" - value: Int +enum A_Float_Enum { + _1_1 + _2_54 + _3 +} - """Timestamp: An int""" - timestamp: String +enum A_Int_Enum { + _1 + _2 + _3 } -"""A float""" -type A_Float { - """Value: A float""" - value: Float +enum A_String_Enum { + JANUARY + FEBRUARY +} - """Timestamp: A float""" - timestamp: String +enum A_StringArray_Enum { + JANUARY + FEBRUARY + MARCH } diff --git a/uv.lock b/uv.lock index 7b531c25..79aa5225 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,9 @@ version = 1 requires-python = ">=3.10" resolution-markers = [ - "python_full_version < '3.13'", + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", + "python_full_version == '3.12.*'", "python_full_version >= '3.13'", ] @@ -198,6 +200,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, ] +[[package]] +name = "graphene" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "graphql-core" }, + { name = "graphql-relay" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/f6/bf62ff950c317ed03e77f3f6ddd7e34aaa98fe89d79ebd660c55343d8054/graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa", size = 44739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/e0/61d8e98007182e6b2aca7cf65904721fb2e4bce0192272ab9cb6f69d8812/graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71", size = 114894 }, +] + [[package]] name = "graphql-core" version = "3.2.5" @@ -207,6 +224,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/dc/078bd6b304de790618ebb95e2aedaadb78f4527ac43a9ad8815f006636b6/graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a", size = 203189 }, ] +[[package]] +name = "graphql-relay" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "graphql-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/13/98fbf8d67552f102488ffc16c6f559ce71ea15f6294728d33928ab5ff14d/graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c", size = 50027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/16/a4cf06adbc711bd364a73ce043b0b08d8fa5aae3df11b6ee4248bcdad2e0/graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5", size = 16940 }, +] + [[package]] name = "identify" version = "2.6.2" @@ -362,6 +391,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/00/981f0dcaddf111b6caf6e03d7f7f01b07fd4af117316a7eb1c22039d9e37/nox-2024.10.9-py3-none-any.whl", hash = "sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab", size = 61210 }, ] +[[package]] +name = "numpy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/80/d572a4737626372915bca41c3afbfec9d173561a39a0a61bacbbfd1dafd4/numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", size = 21152472 }, + { url = "https://files.pythonhosted.org/packages/6f/bb/7bfba10c791ae3bb6716da77ad85a82d5fac07fc96fb0023ef0571df9d20/numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", size = 13747967 }, + { url = "https://files.pythonhosted.org/packages/da/d6/2df7bde35f0478455f0be5934877b3e5a505f587b00230f54a519a6b55a5/numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", size = 5354921 }, + { url = "https://files.pythonhosted.org/packages/d1/bb/75b945874f931494891eac6ca06a1764d0e8208791f3addadb2963b83527/numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", size = 6888603 }, + { url = "https://files.pythonhosted.org/packages/68/a7/fde73636f6498dbfa6d82fc336164635fe592f1ad0d13285fcb6267fdc1c/numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", size = 13889862 }, + { url = "https://files.pythonhosted.org/packages/05/db/5d9c91b2e1e2e72be1369278f696356d44975befcae830daf2e667dcb54f/numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", size = 16328151 }, + { url = "https://files.pythonhosted.org/packages/3e/6a/7eb732109b53ae64a29e25d7e68eb9d6611037f6354875497008a49e74d3/numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", size = 16704107 }, + { url = "https://files.pythonhosted.org/packages/88/cc/278113b66a1141053cbda6f80e4200c6da06b3079c2d27bda1fde41f2c1f/numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4", size = 14385789 }, + { url = "https://files.pythonhosted.org/packages/f5/69/eb20f5e1bfa07449bc67574d2f0f7c1e6b335fb41672e43861a7727d85f2/numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", size = 6536706 }, + { url = "https://files.pythonhosted.org/packages/8e/8b/1c131ab5a94c1086c289c6e1da1d843de9dbd95fe5f5ee6e61904c9518e2/numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", size = 12864165 }, + { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252 }, + { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119 }, + { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978 }, + { url = "https://files.pythonhosted.org/packages/09/ac/61d07930a4993dd9691a6432de16d93bbe6aa4b1c12a5e573d468eefc1ca/numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", size = 6892570 }, + { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715 }, + { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644 }, + { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217 }, + { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053 }, + { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741 }, + { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487 }, + { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 }, + { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 }, + { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 }, + { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 }, + { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 }, + { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 }, + { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 }, + { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 }, + { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 }, + { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 }, + { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 }, + { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 }, + { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 }, + { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 }, + { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 }, + { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 }, + { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 }, + { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 }, + { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 }, + { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 }, + { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 }, + { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 }, + { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 }, + { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 }, + { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 }, + { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 }, + { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 }, + { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 }, + { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 }, + { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 }, + { url = "https://files.pythonhosted.org/packages/00/e7/8d8bb791b62586cc432ecbb70632b4f23b7b7c88df41878de7528264f6d7/numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", size = 20983893 }, + { url = "https://files.pythonhosted.org/packages/5e/f3/cb8118a044b5007586245a650360c9f5915b2f4232dd7658bb7a63dd1d02/numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", size = 6752501 }, + { url = "https://files.pythonhosted.org/packages/53/f5/365b46439b518d2ec6ebb880cc0edf90f225145dfd4db7958334f7164530/numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", size = 16142601 }, + { url = "https://files.pythonhosted.org/packages/03/c2/d1fee6ba999aa7cd41ca6856937f2baaf604c3eec1565eae63451ec31e5e/numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", size = 12771397 }, +] + [[package]] name = "packaging" version = "24.2" @@ -371,6 +462,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -534,6 +673,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -774,6 +934,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + [[package]] name = "virtualenv" version = "20.27.1" @@ -797,9 +966,11 @@ dependencies = [ { name = "bump2version" }, { name = "click" }, { name = "deprecation" }, + { name = "graphene" }, { name = "graphql-core" }, { name = "importlib-metadata" }, { name = "jsonschema" }, + { name = "pandas" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "rdflib" }, @@ -825,9 +996,11 @@ requires-dist = [ { name = "bump2version", specifier = ">=1.0.1" }, { name = "click", specifier = ">=8.1.7" }, { name = "deprecation", specifier = ">=2.1.0" }, + { name = "graphene", specifier = ">=3.4.3" }, { name = "graphql-core", specifier = ">=3.2.5" }, { name = "importlib-metadata", specifier = ">=8.5.0" }, { name = "jsonschema", specifier = ">=4.23.0" }, + { name = "pandas", specifier = ">=2.2.3" }, { name = "pydantic", specifier = ">=2.9.2" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "rdflib", specifier = ">=7.1.1" },