From a613a8617472c9478c15f11c3e1b17e4d348872f Mon Sep 17 00:00:00 2001 From: Etienne Wodey <44871469+airwoodix@users.noreply.github.com> Date: Tue, 25 Apr 2023 14:18:29 +0200 Subject: [PATCH] Use Pydantic models from OpenAPI spec for API requests (#62) * scripts: generate API models from OpenAPI spec. * Use auto-generated API models. --------- Co-authored-by: Wilfried Huss <84843123+wilfried-huss@users.noreply.github.com> --- .pre-commit-config.yaml | 8 + CHANGELOG.md | 1 + api/aqt_public.yml | 824 ++++++++++++++++++++ poetry.lock | 441 ++++++++++- pyproject.toml | 32 +- qiskit_aqt_provider/api_models.py | 171 ++++ qiskit_aqt_provider/api_models_generated.py | 437 +++++++++++ qiskit_aqt_provider/aqt_job.py | 39 +- qiskit_aqt_provider/aqt_resource.py | 58 +- qiskit_aqt_provider/circuit_to_aqt.py | 68 +- qiskit_aqt_provider/test/fixtures.py | 7 +- qiskit_aqt_provider/test/resources.py | 60 +- scripts/api_models.py | 111 +++ test/test_circuit_to_aqt.py | 120 ++- test/test_execution.py | 32 - test/test_resource.py | 74 +- 16 files changed, 2211 insertions(+), 272 deletions(-) create mode 100644 api/aqt_public.yml create mode 100644 qiskit_aqt_provider/api_models.py create mode 100644 qiskit_aqt_provider/api_models_generated.py create mode 100755 scripts/api_models.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae42365..710c71c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,3 +27,11 @@ repos: rev: "0.9.2" hooks: - id: pyproject-fmt + - repo: local + hooks: + - id: check-api-models + name: check generated API models + entry: ./scripts/api_models.py generate + language: script + pass_filenames: false + always_run: true diff --git a/CHANGELOG.md b/CHANGELOG.md index db38d24..61407c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Always raise `TranspilerError` on errors in the custom transpilation passes #57 * Add `AQTSampler`, a specialized implementation of the `Sampler` primitive #60 +* Auto-generate and use Pydantic models for the API requests payloads #62 ## qiskit-aqt-provider v0.12.0 diff --git a/api/aqt_public.yml b/api/aqt_public.yml new file mode 100644 index 0000000..2df43a7 --- /dev/null +++ b/api/aqt_public.yml @@ -0,0 +1,824 @@ +components: + schemas: + Circuit: + description: Json encoding of a quantum circuit. + example: + - operation: RZ + phi: 0.5 + qubit: 0 + - operation: R + phi: 0.25 + qubit: 1 + theta: 0.5 + - operation: RXX + qubits: + - 0 + - 1 + theta: 0.5 + - operation: MEASURE + items: + $ref: '#/components/schemas/OperationModel' + maxItems: 10000 + minItems: 1 + title: Circuit + type: array + GateR: + additionalProperties: false + description: "A single-qubit rotation of angle \u03B8 around axis \u03C6 in\ + \ the equatorial plane\nof the Bloch sphere.\n\nAngles are expressed in units\ + \ of \u03C0." + properties: + operation: + enum: + - R + title: Operation + type: string + phi: + maximum: 2.0 + minimum: 0.0 + title: Phi + type: number + qubit: + minimum: 0.0 + title: Qubit + type: integer + theta: + maximum: 1.0 + minimum: 0.0 + title: Theta + type: number + required: + - qubit + - operation + - phi + - theta + title: GateR + type: object + GateRXX: + additionalProperties: false + description: "A parametric 2-qubits X\u2297X gate with angle \u03B8.\n\nThe\ + \ angle is expressed in units of \u03C0. The gate is maximally entangling\n\ + for \u03B8=0.5 (\u03C0/2)." + properties: + operation: + enum: + - RXX + title: Operation + type: string + qubits: + items: + minimum: 0.0 + type: integer + maxItems: 2 + minItems: 2 + title: Qubits + type: array + uniqueItems: true + theta: + maximum: 0.5 + minimum: 0.0 + title: Theta + type: number + required: + - operation + - qubits + - theta + title: GateRXX + type: object + GateRZ: + additionalProperties: false + description: "A single-qubit rotation of angle \u03C6 around the Z axis of the\ + \ Bloch sphere." + properties: + operation: + enum: + - RZ + title: Operation + type: string + phi: + title: Phi + type: number + qubit: + minimum: 0.0 + title: Qubit + type: integer + required: + - qubit + - operation + - phi + title: GateRZ + type: object + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + title: Detail + type: array + title: HTTPValidationError + type: object + JobResponse_RRCancelled_: + description: This class contains the data a uses is receiving at the "/result" + endpoint. + properties: + job: + $ref: '#/components/schemas/JobUser' + response: + $ref: '#/components/schemas/RRCancelled' + required: + - job + - response + title: JobResponse[RRCancelled] + type: object + JobResponse_RRError_: + description: This class contains the data a uses is receiving at the "/result" + endpoint. + properties: + job: + $ref: '#/components/schemas/JobUser' + response: + $ref: '#/components/schemas/RRError' + required: + - job + - response + title: JobResponse[RRError] + type: object + JobResponse_RRFinished_: + description: This class contains the data a uses is receiving at the "/result" + endpoint. + properties: + job: + $ref: '#/components/schemas/JobUser' + response: + $ref: '#/components/schemas/RRFinished' + required: + - job + - response + title: JobResponse[RRFinished] + type: object + JobResponse_RROngoing_: + description: This class contains the data a uses is receiving at the "/result" + endpoint. + properties: + job: + $ref: '#/components/schemas/JobUser' + response: + $ref: '#/components/schemas/RROngoing' + required: + - job + - response + title: JobResponse[RROngoing] + type: object + JobResponse_RRQueued_: + description: This class contains the data a uses is receiving at the "/result" + endpoint. + properties: + job: + $ref: '#/components/schemas/JobUser' + response: + $ref: '#/components/schemas/RRQueued' + required: + - job + - response + title: JobResponse[RRQueued] + type: object + JobSubmission: + description: Abstract job that can run on a computing resource. + example: + job_type: quantum_circuit + label: Example computation + payload: + number_of_qubits: 3 + quantum_circuit: + - operation: RZ + phi: 0.5 + qubit: 0 + - operation: R + phi: 0.25 + qubit: 1 + theta: 0.5 + - operation: RXX + qubits: + - 0 + - 1 + theta: 0.5 + - operation: MEASURE + repetitions: 5 + properties: + job_type: + default: quantum_circuit + enum: + - quantum_circuit + title: Job Type + type: string + label: + title: Label + type: string + payload: + $ref: '#/components/schemas/QuantumCircuit' + required: + - payload + title: JobSubmission + type: object + JobUser: + description: Abstract job that can run on a computing resource. + example: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + properties: + job_id: + description: Id that uniquely identifies the job. This is used to request + results. + format: uuid + title: Job Id + type: string + job_type: + default: quantum_circuit + enum: + - quantum_circuit + title: Job Type + type: string + label: + title: Label + type: string + resource_id: + default: '' + title: Resource Id + type: string + workspace_id: + default: '' + title: Workspace Id + type: string + required: + - job_id + title: JobUser + type: object + Measure: + additionalProperties: false + description: 'Measurement operation. + + + The MEASURE operation instructs the resource + + to perform a projective measurement of all qubits.' + properties: + operation: + enum: + - MEASURE + title: Operation + type: string + required: + - operation + title: Measure + type: object + OperationModel: + description: 'Model for the items in a Circuit. + + + This extra wrapper is introduced to leverage the pydantic + + tagged-union parser.' + discriminator: + mapping: + MEASURE: '#/components/schemas/Measure' + R: '#/components/schemas/GateR' + RXX: '#/components/schemas/GateRXX' + RZ: '#/components/schemas/GateRZ' + propertyName: operation + oneOf: + - $ref: '#/components/schemas/GateRZ' + - $ref: '#/components/schemas/GateR' + - $ref: '#/components/schemas/GateRXX' + - $ref: '#/components/schemas/Measure' + title: OperationModel + QuantumCircuit: + description: A quantum circuit-type job that can run on a computing resource. + example: + number_of_qubits: 3 + quantum_circuit: + - operation: RZ + phi: 0.5 + qubit: 0 + - operation: R + phi: 0.25 + qubit: 1 + theta: 0.5 + - operation: RXX + qubits: + - 0 + - 1 + theta: 0.5 + - operation: MEASURE + repetitions: 5 + properties: + number_of_qubits: + exclusiveMinimum: 0.0 + title: Number Of Qubits + type: integer + quantum_circuit: + $ref: '#/components/schemas/Circuit' + repetitions: + exclusiveMinimum: 0.0 + title: Repetitions + type: integer + required: + - repetitions + - quantum_circuit + - number_of_qubits + title: QuantumCircuit + type: object + RRCancelled: + example: + status: cancelled + properties: + status: + default: cancelled + enum: + - cancelled + title: Status + type: string + title: RRCancelled + type: object + RRError: + example: + message: detailed error message + status: error + properties: + message: + title: Message + type: string + status: + default: error + enum: + - error + title: Status + type: string + required: + - message + title: RRError + type: object + RRFinished: + description: Contains the measurement data of a finished circuit. + example: + result: + - - 0 + - 1 + - 1 + - - 1 + - 1 + - 1 + - - 0 + - 0 + - 0 + - - 1 + - 1 + - 0 + - - 1 + - 1 + - 0 + status: finished + properties: + result: + items: + items: + maximum: 1.0 + minimum: 0.0 + type: integer + type: array + title: Result + type: array + status: + default: finished + enum: + - finished + title: Status + type: string + required: + - result + title: RRFinished + type: object + RROngoing: + example: + status: ongoing + properties: + status: + default: ongoing + enum: + - ongoing + title: Status + type: string + title: RROngoing + type: object + RRQueued: + example: + status: queued + properties: + status: + default: queued + enum: + - queued + title: Status + type: string + title: RRQueued + type: object + Resource: + examples: + device: + description: Ion trapped quantum computer + summary: Quantum device + value: + id: simulator_noise + name: Simulator with noise + type: simulator + simulator: + description: Quantum computing simulator + summary: Simulator device + value: + id: simulator + name: Simulator without noise + type: simulator + properties: + id: + title: Id + type: string + name: + title: Name + type: string + type: + enum: + - simulator + - device + title: Type + type: string + required: + - name + - id + - type + title: Resource + type: object + ResultResponse: + anyOf: + - $ref: '#/components/schemas/JobResponse_RRQueued_' + - $ref: '#/components/schemas/JobResponse_RROngoing_' + - $ref: '#/components/schemas/JobResponse_RRFinished_' + - $ref: '#/components/schemas/JobResponse_RRError_' + - $ref: '#/components/schemas/JobResponse_RRCancelled_' + - $ref: '#/components/schemas/UnknownJob' + examples: + cancelled: + description: Job that has been cancelled by the user, before it could be + processed by the Quantum computer + summary: Cancelled Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + status: cancelled + error: + description: Job that created an error while being processed by the Quantum + computer + summary: Failed Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + message: detailed error message + status: error + finished: + description: Job that has been successfully processed by a quantum computer + or simulator + summary: Finished Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + result: + - - 1 + - 0 + - 0 + - - 1 + - 1 + - 0 + - - 0 + - 0 + - 0 + - - 1 + - 1 + - 0 + - - 1 + - 1 + - 0 + status: finished + ongoing: + description: Job that is currently being processed by the Quantum computer + summary: Ongoing Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + status: ongoing + queued: + description: Job waiting in the queue to be picked up by the Quantum computer + summary: Queued Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + status: cancelled + unknown: + description: The supplied job id could not be found + summary: Unknown Job + value: + job_id: 3aa8b827-4ff0-4a36-b1a6-f9ff6dee59ce + message: unknown job_id + title: ResultResponse + UnknownJob: + properties: + job_id: + format: uuid + title: Job Id + type: string + message: + default: unknown job_id + enum: + - unknown job_id + title: Message + type: string + required: + - job_id + title: UnknownJob + type: object + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + title: Location + type: array + msg: + title: Message + type: string + type: + title: Error Type + type: string + required: + - loc + - msg + - type + title: ValidationError + type: object + Workspace: + example: + id: default + resources: + - id: simulator + name: Simulator without noise + type: simulator + - id: simulator_noise + name: Simulator with noise + type: simulator + properties: + id: + title: Id + type: string + resources: + items: + $ref: '#/components/schemas/Resource' + title: Resources + type: array + required: + - id + - resources + title: Workspace + type: object +info: + title: AQT Public API + version: 0.2.0 +openapi: 3.0.2 +paths: + /result/{job_id}: + get: + description: Request job results + operationId: request_result_result__job_id__get + parameters: + - in: path + name: job_id + required: true + schema: + format: uuid + title: Job Id + type: string + - description: AQT cloud access token + in: header + name: authorization + required: false + schema: + default: token + description: AQT cloud access token + title: Authorization + type: string + responses: + '200': + content: + application/json: + examples: + cancelled: + description: Job that has been cancelled by the user, before it + could be processed by the Quantum computer + summary: Cancelled Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + status: cancelled + error: + description: Job that created an error while being processed by + the Quantum computer + summary: Failed Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + message: detailed error message + status: error + finished: + description: Job that has been successfully processed by a quantum + computer or simulator + summary: Finished Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + result: + - - 1 + - 0 + - 0 + - - 1 + - 1 + - 0 + - - 0 + - 0 + - 0 + - - 1 + - 1 + - 0 + - - 1 + - 1 + - 0 + status: finished + ongoing: + description: Job that is currently being processed by the Quantum + computer + summary: Ongoing Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + status: ongoing + queued: + description: Job waiting in the queue to be picked up by the Quantum + computer + summary: Queued Job + value: + job: + job_id: ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450 + job_type: quantum_circuit + label: Example computation + resource_id: '' + workspace_id: '' + response: + status: cancelled + unknown: + description: The supplied job id could not be found + summary: Unknown Job + value: + job_id: 3aa8b827-4ff0-4a36-b1a6-f9ff6dee59ce + message: unknown job_id + schema: + $ref: '#/components/schemas/ResultResponse' + description: Success + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Request Result + /submit/{workspace}/{resource}: + post: + description: Submit a quantum circuit. + operationId: submit_job_submit__workspace___resource__post + parameters: + - in: path + name: workspace + required: true + schema: + title: Workspace + type: string + - in: path + name: resource + required: true + schema: + title: Resource + type: string + - description: AQT cloud access token + in: header + name: authorization + required: false + schema: + default: token + description: AQT cloud access token + title: Authorization + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/JobSubmission' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/JobResponse_RRQueued_' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Submit Job + /workspaces: + get: + description: List of available workspaces and devices + operationId: workspaces_workspaces_get + parameters: + - description: AQT cloud access token + in: header + name: authorization + required: false + schema: + default: token + description: AQT cloud access token + title: Authorization + type: string + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Workspace' + title: Response Workspaces Workspaces Get + type: array + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Workspaces diff --git a/poetry.lock b/poetry.lock index 9ce312e..1671518 100644 --- a/poetry.lock +++ b/poetry.lock @@ -45,6 +45,21 @@ files = [ {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, ] +[[package]] +name = "argcomplete" +version = "3.0.8" +description = "Bash tab completion for argparse" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argcomplete-3.0.8-py3-none-any.whl", hash = "sha256:e36fd646839933cbec7941c662ecb65338248667358dd3d968405a4506a60d9b"}, + {file = "argcomplete-3.0.8.tar.gz", hash = "sha256:b9ca96448e14fa459d7450a4ab5a22bbf9cee4ba7adddf03e65c398b5daeea28"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + [[package]] name = "asteval" version = "0.9.29" @@ -316,6 +331,18 @@ files = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] +[[package]] +name = "chardet" +version = "5.1.0" +description = "Universal encoding detector for Python 3" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"}, + {file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"}, +] + [[package]] name = "charset-normalizer" version = "3.1.0" @@ -641,6 +668,43 @@ files = [ {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, ] +[[package]] +name = "datamodel-code-generator" +version = "0.0.0" +description = "Datamodel Code Generator" +category = "dev" +optional = false +python-versions = "^3.7" +files = [] +develop = false + +[package.dependencies] +argcomplete = ">=1.10,<4.0" +black = ">=19.10b0" +genson = ">=1.2.1,<2.0" +inflect = ">=4.1.0,<6.0" +isort = ">=4.3.21,<6.0" +jinja2 = ">=2.10.1,<4.0" +openapi-spec-validator = ">=0.2.8,<=0.5.1" +packaging = "*" +prance = ">=0.18.2,<1.0" +pydantic = [ + {version = "^1.5.1", extras = ["email"], markers = "python_version < \"3.10\""}, + {version = "^1.9.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = "^1.10.0", extras = ["email"], markers = "python_version >= \"3.11\" and python_version < \"4.0\""}, +] +PySnooper = ">=0.4.1,<2.0.0" +toml = ">=0.10.0,<1.0.0" + +[package.extras] +http = ["httpx"] + +[package.source] +type = "git" +url = "https://github.com/airwoodix/datamodel-code-generator" +reference = "annotated-backport" +resolved_reference = "396713b2da87df6babf42389eb943ab316ca0b3d" + [[package]] name = "debugpy" version = "1.6.7" @@ -736,6 +800,27 @@ files = [ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] +[[package]] +name = "dnspython" +version = "2.3.0" +description = "DNS toolkit" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, + {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, +] + +[package.extras] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +dnssec = ["cryptography (>=2.6,<40.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] +doq = ["aioquic (>=0.9.20)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.23)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + [[package]] name = "docutils" version = "0.19" @@ -748,6 +833,22 @@ files = [ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] +[[package]] +name = "email-validator" +version = "2.0.0.post2" +description = "A robust email address syntax and deliverability validation library." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "email_validator-2.0.0.post2-py3-none-any.whl", hash = "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c"}, + {file = "email_validator-2.0.0.post2.tar.gz", hash = "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "exceptiongroup" version = "1.1.1" @@ -846,6 +947,17 @@ files = [ {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, ] +[[package]] +name = "genson" +version = "1.2.2" +description = "GenSON is a powerful, user-friendly JSON Schema generator." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "genson-1.2.2.tar.gz", hash = "sha256:8caf69aa10af7aee0e1a1351d1d06801f4696e005f06cedef438635384346a16"}, +] + [[package]] name = "h11" version = "0.14.0" @@ -1015,6 +1127,22 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +[[package]] +name = "inflect" +version = "5.6.2" +description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "inflect-5.6.2-py3-none-any.whl", hash = "sha256:b45d91a4a28a4e617ff1821117439b06eaa86e2a4573154af0149e9be6687238"}, + {file = "inflect-5.6.2.tar.gz", hash = "sha256:aadc7ed73928f5e014129794bbac03058cca35d0a973a5fc4eb45c7fa26005f9"}, +] + +[package.extras] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["pygments", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -1123,6 +1251,24 @@ widgetsnbextension = ">=4.0.7,<4.1.0" [package.extras] test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + [[package]] name = "jedi" version = "0.18.2" @@ -1183,6 +1329,24 @@ pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] +[[package]] +name = "jsonschema-spec" +version = "0.1.4" +description = "JSONSchema Spec with object-oriented paths" +category = "dev" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "jsonschema_spec-0.1.4-py3-none-any.whl", hash = "sha256:34471d8b60e1f06d174236c4d3cf9590fbf3cff1cc733b28d15cd83672bcd062"}, + {file = "jsonschema_spec-0.1.4.tar.gz", hash = "sha256:824c743197bbe2104fcc6dce114a4082bf7f7efdebf16683510cb0ec6d8d53d0"}, +] + +[package.dependencies] +jsonschema = ">=4.0.0,<4.18.0" +pathable = ">=0.4.1,<0.5.0" +PyYAML = ">=5.1" +typing-extensions = ">=4.3.0,<5.0.0" + [[package]] name = "jupyter-client" version = "8.2.0" @@ -1349,6 +1513,52 @@ files = [ {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, ] +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] + [[package]] name = "lmfit" version = "1.2.0" @@ -1761,6 +1971,50 @@ files = [ {file = "numpy-1.23.5.tar.gz", hash = "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a"}, ] +[[package]] +name = "openapi-schema-validator" +version = "0.3.4" +description = "OpenAPI schema validation for Python" +category = "dev" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "openapi-schema-validator-0.3.4.tar.gz", hash = "sha256:7cf27585dd7970b7257cefe48e1a3a10d4e34421831bdb472d96967433bc27bd"}, + {file = "openapi_schema_validator-0.3.4-py3-none-any.whl", hash = "sha256:34fbd14b7501abe25e64d7b4624a9db02cde1a578d285b3da6f34b290cdf0b3a"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +jsonschema = ">=4.0.0,<5.0.0" + +[package.extras] +isodate = ["isodate"] +rfc3339-validator = ["rfc3339-validator"] +strict-rfc3339 = ["strict-rfc3339"] + +[[package]] +name = "openapi-spec-validator" +version = "0.5.1" +description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" +category = "dev" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "openapi-spec-validator-0.5.1.tar.gz", hash = "sha256:8248634bad1f23cac5d5a34e193ab36e23914057ca69e91a1ede5af75552c465"}, + {file = "openapi_spec_validator-0.5.1-py3-none-any.whl", hash = "sha256:4a8aee1e45b1ac868e07ab25e18828fe9837baddd29a8e20fdb3d3c61c8eea3d"}, +] + +[package.dependencies] +importlib-resources = ">=5.8.0,<6.0.0" +jsonschema = ">=4.0.0,<5.0.0" +jsonschema-spec = ">=0.1.1,<0.2.0" +lazy-object-proxy = ">=1.7.1,<2.0.0" +openapi-schema-validator = ">=0.3.2,<0.4.0" +PyYAML = ">=5.1" + +[package.extras] +requests = ["requests"] + [[package]] name = "packaging" version = "23.1" @@ -1881,6 +2135,18 @@ files = [ {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, ] +[[package]] +name = "pathable" +version = "0.4.3" +description = "Object-oriented paths" +category = "dev" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14"}, + {file = "pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab"}, +] + [[package]] name = "pathspec" version = "0.11.1" @@ -2087,6 +2353,33 @@ tomli = ">=1.2.2" [package.extras] poetry-plugin = ["poetry (>=1.0,<2.0)"] +[[package]] +name = "prance" +version = "0.22.2.22.0" +description = "Resolving Swagger/OpenAPI 2.0 and 3.0.0 Parser" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "prance-0.22.2.22.0-py3-none-any.whl", hash = "sha256:57deeb67b7e93ef27c1c17845bf3ccb4af288ccfb5748c7e01779c01a8507f27"}, + {file = "prance-0.22.2.22.0.tar.gz", hash = "sha256:9a83f8a4f5fe0f2d896d238d4bec6b5788b10b94155414b3d88c21c1579b85bf"}, +] + +[package.dependencies] +chardet = ">=3.0" +packaging = ">=21.3" +requests = ">=2.25" +"ruamel.yaml" = ">=0.17.10" +six = ">=1.15,<2.0" + +[package.extras] +cli = ["click (>=7.0)"] +dev = ["bumpversion (>=0.6)", "pytest (>=6.1)", "pytest-cov (>=2.11)", "sphinx (>=3.4)", "towncrier (>=19.2)", "tox (>=3.4)"] +flex = ["flex (>=6.13,<7.0)"] +icu = ["PyICU (>=2.4,<3.0)"] +osv = ["openapi-spec-validator (>=0.5.1,<0.6.0)"] +ssv = ["swagger-spec-validator (>=2.4,<3.0)"] + [[package]] name = "pre-commit" version = "3.2.2" @@ -2187,6 +2480,60 @@ files = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +[[package]] +name = "pydantic" +version = "1.10.7" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, + {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, + {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, + {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, + {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, + {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, + {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, + {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, + {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, +] + +[package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + [[package]] name = "pydot" version = "1.4.2" @@ -2319,6 +2666,21 @@ files = [ {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, ] +[[package]] +name = "pysnooper" +version = "1.1.1" +description = "A poor man's debugger for Python." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "PySnooper-1.1.1-py2.py3-none-any.whl", hash = "sha256:378f13d731a3e04d3d0350e5f295bdd0f1b49fc8a8b8bf2067fe1e5290bd20be"}, + {file = "PySnooper-1.1.1.tar.gz", hash = "sha256:d17dc91cca1593c10230dce45e46b1d3ff0f8910f0c38e941edf6ba1260b3820"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "pyspnego" version = "0.8.0" @@ -2875,6 +3237,71 @@ cryptography = ">=1.3" pyspnego = ">=0.1.6" requests = ">=2.0.0" +[[package]] +name = "ruamel-yaml" +version = "0.17.21" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "dev" +optional = false +python-versions = ">=3" +files = [ + {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, + {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.7" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, + {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, +] + [[package]] name = "ruff" version = "0.0.262" @@ -3385,6 +3812,18 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["flake8", "isort", "pytest"] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -3824,4 +4263,4 @@ examples = ["tweedledum"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "8590027bf64627f4d695f1df9751519b9a5fc2aff5ba0128c769d9b8262e5f19" +content-hash = "be0c9c75f37a91995e12fffcff428ed661262c13bcf70fc8abadb06d39d03be6" diff --git a/pyproject.toml b/pyproject.toml index 0808151..e9b4aa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ pytest_qiskit_aqt = "qiskit_aqt_provider.test.fixtures" python = "^3.8" httpx = ">=0.24.0" +pydantic = ">=1.10,<2" python-dotenv = ">=1" qiskit-aer = ">=0.11" qiskit-terra = ">=0.23.3" @@ -55,6 +56,7 @@ typing-extensions = ">=4.0.0" [tool.poetry.group.dev.dependencies] black = "^23.3.0" coverage = "^7.2.1" +datamodel-code-generator = { git = "https://github.com/airwoodix/datamodel-code-generator", branch = "annotated-backport" } dirty-equals = "^0.5.0" hypothesis = "^6.70.0" ipykernel = "^6.22.0" @@ -91,9 +93,22 @@ requires = [ "poetry-core>=1", ] +[tool.datamodel-codegen] +disable-timestamp = true +enable-faux-immutability = true +enum-field-as-literal = "one" +field-constraints = true +strict-nullable = true +target-python-version = '3.8' +use-annotated = true +use-double-quotes = true +use-schema-description = true +use-field-description = true +wrap-string-literal = true + [tool.black] line-length = 100 -target-version = ['py37', 'py38', 'py39', 'py310'] +target-version = ['py38', 'py39', 'py310'] preview = true # don't use implicit string concatenation [tool.isort] @@ -110,7 +125,7 @@ typeCheckingMode = "basic" analyzeUnannotatedFunctions = false reportShadowedImports = true reportTypeCommentUsage = true -reportImportCycles = true +reportImportCycles = false reportMissingImports = false reportMissingTypeStubs = false reportConstantRedefinition = true @@ -164,6 +179,9 @@ ignore = [ ] line-length = 100 target-version = 'py38' +extend-exclude = [ + "qiskit_aqt_provider/api_models_generated.py", # generated code +] [tool.ruff.per-file-ignores] "examples/*.py" = [ @@ -182,7 +200,7 @@ target-version = 'py38' convention = "google" [tool.coverage.report] -fail_under = 97 +fail_under = 98 [tool.poe.tasks.test] shell = """ @@ -212,7 +230,13 @@ mypy . pyright . """ +[tool.poe.tasks.lint] +shell = """ +ruff check . +./scripts/api_models.py check +""" + [tool.poe.tasks] -lint = "ruff check ." +generate-models = "./scripts/api_models.py generate" docs = "sphinx-build -b html -W docs docs/_build" all = ["format_check", "lint", "typecheck", "test", "docs"] diff --git a/qiskit_aqt_provider/api_models.py b/qiskit_aqt_provider/api_models.py new file mode 100644 index 0000000..6751c5c --- /dev/null +++ b/qiskit_aqt_provider/api_models.py @@ -0,0 +1,171 @@ +# This code is part of Qiskit. +# +# (C) Copyright Alpine Quantum Technologies GmbH 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Thin convenience wrappers around generated API models.""" + +from typing import List, Union +from uuid import UUID + +from qiskit.providers.exceptions import JobError +from typing_extensions import TypeAlias + +from qiskit_aqt_provider import api_models_generated as api_models +from qiskit_aqt_provider.api_models_generated import ( + Circuit, + JobSubmission, + OperationModel, + QuantumCircuit, + ResultResponse, +) + +__all__ = [ + "Circuit", + "Operation", + "OperationModel", + "JobSubmission", + "QuantumCircuit", + "ResultResponse", + "Response", + "JobResponse", +] + + +class UnknownJobError(JobError): + """Requested an unknown job to the AQT API.""" + + +class Operation: + """Factories for API payloads of circuit operations.""" + + @staticmethod + def rz(*, phi: float, qubit: int) -> api_models.OperationModel: + """RZ gate.""" + return api_models.OperationModel( + __root__=api_models.GateRZ(operation="RZ", phi=phi, qubit=qubit) + ) + + @staticmethod + def r(*, phi: float, theta: float, qubit: int) -> api_models.OperationModel: + """R gate.""" + return api_models.OperationModel( + __root__=api_models.GateR(operation="R", phi=phi, theta=theta, qubit=qubit) + ) + + @staticmethod + def rxx(*, theta: float, qubits: List[int]) -> api_models.OperationModel: + """RXX gate.""" + return api_models.OperationModel( + __root__=api_models.GateRXX( + operation="RXX", + theta=theta, + qubits=[api_models.Qubit(__root__=qubit) for qubit in qubits], + ) + ) + + @staticmethod + def measure() -> api_models.OperationModel: + """MEASURE operation.""" + return api_models.OperationModel(__root__=api_models.Measure(operation="MEASURE")) + + +JobResponse: TypeAlias = Union[ + api_models.JobResponseRRQueued, + api_models.JobResponseRROngoing, + api_models.JobResponseRRFinished, + api_models.JobResponseRRError, + api_models.JobResponseRRCancelled, +] + + +class Response: + """Factories for API response payloads.""" + + @staticmethod + def parse_raw(data: str) -> JobResponse: + """Parse an API response. + + Returns: + The corresponding JobResponse object. + + Raises: + UnknownJobError: the server answered with an unknown job error. + """ + response = ResultResponse.parse_raw(data).__root__ + + if isinstance(response, api_models.UnknownJob): + raise UnknownJobError(str(response.job_id)) + + return response + + @staticmethod + def queued(*, job_id: UUID, workspace_id: str, resource_id: str) -> JobResponse: + """Queued job.""" + return api_models.JobResponseRRQueued( + job=api_models.JobUser( + job_id=job_id, label="qiskit", resource_id=resource_id, workspace_id=workspace_id + ), + response=api_models.RRQueued(), + ) + + @staticmethod + def ongoing(*, job_id: UUID, workspace_id: str, resource_id: str) -> JobResponse: + """Ongoing job.""" + return api_models.JobResponseRROngoing( + job=api_models.JobUser( + job_id=job_id, label="qiskit", resource_id=resource_id, workspace_id=workspace_id + ), + response=api_models.RROngoing(), + ) + + @staticmethod + def finished( + *, job_id: UUID, workspace_id: str, resource_id: str, samples: List[List[int]] + ) -> JobResponse: + """Completed job with `samples` as result.""" + return api_models.JobResponseRRFinished( + job=api_models.JobUser( + job_id=job_id, + label="qiskit", + resource_id=resource_id, + workspace_id=workspace_id, + ), + response=api_models.RRFinished( + result=[ + [api_models.ResultItem(__root__=state) for state in shot] for shot in samples + ] + ), + ) + + @staticmethod + def error(*, job_id: UUID, workspace_id: str, resource_id: str, message: str) -> JobResponse: + """Failed job.""" + return api_models.JobResponseRRError( + job=api_models.JobUser( + job_id=job_id, label="qiskit", resource_id=resource_id, workspace_id=workspace_id + ), + response=api_models.RRError(message=message), + ) + + @staticmethod + def cancelled(*, job_id: UUID, workspace_id: str, resource_id: str) -> JobResponse: + """Cancelled job.""" + return api_models.JobResponseRRCancelled( + job=api_models.JobUser( + job_id=job_id, label="qiskit", resource_id=resource_id, workspace_id=workspace_id + ), + response=api_models.RRCancelled(), + ) + + @staticmethod + def unknown_job(*, job_id: UUID) -> api_models.UnknownJob: + """Unknown job.""" + return api_models.UnknownJob(job_id=job_id) diff --git a/qiskit_aqt_provider/api_models_generated.py b/qiskit_aqt_provider/api_models_generated.py new file mode 100644 index 0000000..3ce7ede --- /dev/null +++ b/qiskit_aqt_provider/api_models_generated.py @@ -0,0 +1,437 @@ +# generated by datamodel-codegen: +# filename: aqt_public.yml + +from __future__ import annotations + +from enum import Enum +from typing import List, Literal, Optional, Union +from uuid import UUID + +from pydantic import BaseModel, Extra, Field +from typing_extensions import Annotated + + +class GateR(BaseModel): + """ + A single-qubit rotation of angle θ around axis φ in the equatorial plane + of the Bloch sphere. + + Angles are expressed in units of π. + """ + + class Config: + extra = Extra.forbid + allow_mutation = False + + operation: Annotated[Literal["R"], Field(title="Operation")] + phi: Annotated[float, Field(ge=0.0, le=2.0, title="Phi")] + qubit: Annotated[int, Field(ge=0, title="Qubit")] + theta: Annotated[float, Field(ge=0.0, le=1.0, title="Theta")] + + +class Qubit(BaseModel): + class Config: + allow_mutation = False + + __root__: Annotated[int, Field(ge=0)] + + +class GateRXX(BaseModel): + """ + A parametric 2-qubits X⊗X gate with angle θ. + + The angle is expressed in units of π. The gate is maximally entangling + for θ=0.5 (π/2). + """ + + class Config: + extra = Extra.forbid + allow_mutation = False + + operation: Annotated[Literal["RXX"], Field(title="Operation")] + qubits: Annotated[ + List[Qubit], Field(max_items=2, min_items=2, title="Qubits", unique_items=True) + ] + theta: Annotated[float, Field(ge=0.0, le=0.5, title="Theta")] + + +class GateRZ(BaseModel): + """ + A single-qubit rotation of angle φ around the Z axis of the Bloch sphere. + """ + + class Config: + extra = Extra.forbid + allow_mutation = False + + operation: Annotated[Literal["RZ"], Field(title="Operation")] + phi: Annotated[float, Field(title="Phi")] + qubit: Annotated[int, Field(ge=0, title="Qubit")] + + +class JobUser(BaseModel): + """ + Abstract job that can run on a computing resource. + """ + + class Config: + allow_mutation = False + + job_id: Annotated[UUID, Field(title="Job Id")] + """ + Id that uniquely identifies the job. This is used to request results. + """ + job_type: Annotated[Literal["quantum_circuit"], Field(title="Job Type")] = "quantum_circuit" + label: Annotated[Optional[str], Field(title="Label")] = None + resource_id: Annotated[str, Field(title="Resource Id")] = "" + workspace_id: Annotated[str, Field(title="Workspace Id")] = "" + + +class Measure(BaseModel): + """ + Measurement operation. + + The MEASURE operation instructs the resource + to perform a projective measurement of all qubits. + """ + + class Config: + extra = Extra.forbid + allow_mutation = False + + operation: Annotated[Literal["MEASURE"], Field(title="Operation")] + + +class OperationModel(BaseModel): + class Config: + allow_mutation = False + + __root__: Annotated[ + Union[GateRZ, GateR, GateRXX, Measure], + Field(discriminator="operation", title="OperationModel"), + ] + """ + Model for the items in a Circuit. + + This extra wrapper is introduced to leverage the pydantic + tagged-union parser. + """ + + +class RRCancelled(BaseModel): + class Config: + allow_mutation = False + + status: Annotated[Literal["cancelled"], Field(title="Status")] = "cancelled" + + +class RRError(BaseModel): + class Config: + allow_mutation = False + + message: Annotated[str, Field(title="Message")] + status: Annotated[Literal["error"], Field(title="Status")] = "error" + + +class ResultItem(BaseModel): + class Config: + allow_mutation = False + + __root__: Annotated[int, Field(ge=0, le=1)] + + +class RRFinished(BaseModel): + """ + Contains the measurement data of a finished circuit. + """ + + class Config: + allow_mutation = False + + result: Annotated[List[List[ResultItem]], Field(title="Result")] + status: Annotated[Literal["finished"], Field(title="Status")] = "finished" + + +class RROngoing(BaseModel): + class Config: + allow_mutation = False + + status: Annotated[Literal["ongoing"], Field(title="Status")] = "ongoing" + + +class RRQueued(BaseModel): + class Config: + allow_mutation = False + + status: Annotated[Literal["queued"], Field(title="Status")] = "queued" + + +class Type(Enum): + simulator = "simulator" + device = "device" + + +class Resource(BaseModel): + class Config: + allow_mutation = False + + id: Annotated[str, Field(title="Id")] + name: Annotated[str, Field(title="Name")] + type: Annotated[Type, Field(title="Type")] + + +class UnknownJob(BaseModel): + class Config: + allow_mutation = False + + job_id: Annotated[UUID, Field(title="Job Id")] + message: Annotated[Literal["unknown job_id"], Field(title="Message")] = "unknown job_id" + + +class ValidationError(BaseModel): + class Config: + allow_mutation = False + + loc: Annotated[List[Union[str, int]], Field(title="Location")] + msg: Annotated[str, Field(title="Message")] + type: Annotated[str, Field(title="Error Type")] + + +class Workspace(BaseModel): + class Config: + allow_mutation = False + + id: Annotated[str, Field(title="Id")] + resources: Annotated[List[Resource], Field(title="Resources")] + + +class Circuit(BaseModel): + """ + Json encoding of a quantum circuit. + """ + + class Config: + allow_mutation = False + + __root__: Annotated[ + List[OperationModel], + Field( + example=[ + {"operation": "RZ", "phi": 0.5, "qubit": 0}, + {"operation": "R", "phi": 0.25, "qubit": 1, "theta": 0.5}, + {"operation": "RXX", "qubits": [0, 1], "theta": 0.5}, + {"operation": "MEASURE"}, + ], + max_items=10000, + min_items=1, + title="Circuit", + ), + ] + """ + Json encoding of a quantum circuit. + """ + + +class HTTPValidationError(BaseModel): + class Config: + allow_mutation = False + + detail: Annotated[Optional[List[ValidationError]], Field(title="Detail")] = None + + +class JobResponseRRCancelled(BaseModel): + """ + This class contains the data a uses is receiving at the "/result" endpoint. + """ + + class Config: + allow_mutation = False + + job: JobUser + response: RRCancelled + + +class JobResponseRRError(BaseModel): + """ + This class contains the data a uses is receiving at the "/result" endpoint. + """ + + class Config: + allow_mutation = False + + job: JobUser + response: RRError + + +class JobResponseRRFinished(BaseModel): + """ + This class contains the data a uses is receiving at the "/result" endpoint. + """ + + class Config: + allow_mutation = False + + job: JobUser + response: RRFinished + + +class JobResponseRROngoing(BaseModel): + """ + This class contains the data a uses is receiving at the "/result" endpoint. + """ + + class Config: + allow_mutation = False + + job: JobUser + response: RROngoing + + +class JobResponseRRQueued(BaseModel): + """ + This class contains the data a uses is receiving at the "/result" endpoint. + """ + + class Config: + allow_mutation = False + + job: JobUser + response: RRQueued + + +class QuantumCircuit(BaseModel): + """ + A quantum circuit-type job that can run on a computing resource. + """ + + class Config: + allow_mutation = False + + number_of_qubits: Annotated[int, Field(gt=0, title="Number Of Qubits")] + quantum_circuit: Circuit + repetitions: Annotated[int, Field(gt=0, title="Repetitions")] + + +class ResultResponse(BaseModel): + class Config: + allow_mutation = False + + __root__: Annotated[ + Union[ + JobResponseRRQueued, + JobResponseRROngoing, + JobResponseRRFinished, + JobResponseRRError, + JobResponseRRCancelled, + UnknownJob, + ], + Field( + examples={ + "cancelled": { + "description": ( + "Job that has been cancelled by the user, before it could be processed by" + " the Quantum computer" + ), + "summary": "Cancelled Job", + "value": { + "job": { + "job_id": "ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450", + "job_type": "quantum_circuit", + "label": "Example computation", + "resource_id": "", + "workspace_id": "", + }, + "response": {"status": "cancelled"}, + }, + }, + "error": { + "description": ( + "Job that created an error while being processed by the Quantum computer" + ), + "summary": "Failed Job", + "value": { + "job": { + "job_id": "ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450", + "job_type": "quantum_circuit", + "label": "Example computation", + "resource_id": "", + "workspace_id": "", + }, + "response": {"message": "detailed error message", "status": "error"}, + }, + }, + "finished": { + "description": ( + "Job that has been successfully processed by a quantum computer or" + " simulator" + ), + "summary": "Finished Job", + "value": { + "job": { + "job_id": "ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450", + "job_type": "quantum_circuit", + "label": "Example computation", + "resource_id": "", + "workspace_id": "", + }, + "response": { + "result": [[1, 0, 0], [1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 1, 0]], + "status": "finished", + }, + }, + }, + "ongoing": { + "description": "Job that is currently being processed by the Quantum computer", + "summary": "Ongoing Job", + "value": { + "job": { + "job_id": "ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450", + "job_type": "quantum_circuit", + "label": "Example computation", + "resource_id": "", + "workspace_id": "", + }, + "response": {"status": "ongoing"}, + }, + }, + "queued": { + "description": ( + "Job waiting in the queue to be picked up by the Quantum computer" + ), + "summary": "Queued Job", + "value": { + "job": { + "job_id": "ccaa39de-d0f3-4c8b-bdb1-4d74f0c2f450", + "job_type": "quantum_circuit", + "label": "Example computation", + "resource_id": "", + "workspace_id": "", + }, + "response": {"status": "cancelled"}, + }, + }, + "unknown": { + "description": "The supplied job id could not be found", + "summary": "Unknown Job", + "value": { + "job_id": "3aa8b827-4ff0-4a36-b1a6-f9ff6dee59ce", + "message": "unknown job_id", + }, + }, + }, + title="ResultResponse", + ), + ] + + +class JobSubmission(BaseModel): + """ + Abstract job that can run on a computing resource. + """ + + class Config: + allow_mutation = False + + job_type: Annotated[Literal["quantum_circuit"], Field(title="Job Type")] = "quantum_circuit" + label: Annotated[Optional[str], Field(title="Label")] = None + payload: QuantumCircuit diff --git a/qiskit_aqt_provider/aqt_job.py b/qiskit_aqt_provider/aqt_job.py index b49abb9..8949f0e 100644 --- a/qiskit_aqt_provider/aqt_job.py +++ b/qiskit_aqt_provider/aqt_job.py @@ -33,14 +33,11 @@ from qiskit.providers.jobstatus import JobStatus from qiskit.result.result import Result from qiskit.utils.lazy_tester import contextlib +from typing_extensions import assert_never -if TYPE_CHECKING: # pragma: no cover - # Pyright also reports import cycles, even if guarded by TYPE_CHECKING - # (see https://github.com/microsoft/pylance-release/issues/531). - # The following comment disables import cycles detection for the whole - # file but really only should apply to this block. +from qiskit_aqt_provider import api_models_generated - # pyright: reportImportCycles=false +if TYPE_CHECKING: # pragma: no cover from qiskit_aqt_provider.aqt_resource import AQTResource @@ -103,7 +100,7 @@ def __init__( self.circuits = circuits self._jobs: Dict[ - str, Union[JobFinished, JobFailed, JobQueued, JobOngoing, JobCancelled] + uuid.UUID, Union[JobFinished, JobFailed, JobQueued, JobOngoing, JobCancelled] ] = {} self._jobs_lock = threading.Lock() @@ -197,12 +194,12 @@ def result(self) -> Result: ) @property - def job_ids(self) -> Set[str]: + def job_ids(self) -> Set[uuid.UUID]: """The AQT API identifiers of all the circuits evaluated in this Qiskit job.""" return set(self._jobs) @property - def failed_jobs(self) -> Dict[str, str]: + def failed_jobs(self) -> Dict[uuid.UUID, str]: """Map of failed job ids to error reports from the API.""" with self._jobs_lock: return { @@ -225,27 +222,29 @@ def _submit_single(self, circuit: QuantumCircuit, shots: int) -> None: with self._jobs_lock: self._jobs[job_id] = JobQueued() - def _status_single(self, job_id: str) -> None: + def _status_single(self, job_id: uuid.UUID) -> None: """Query the status of a single circuit execution. This method updates the internal life-cycle tracker. """ payload = self._backend.result(job_id) - response = payload["response"] with self._jobs_lock: - if response["status"] == "finished": - self._jobs[job_id] = JobFinished(samples=response["result"]) - elif response["status"] == "error": - self._jobs[job_id] = JobFailed(error=str(response["message"])) - elif response["status"] == "queued": + # TODO: why don't user-defined TypeGuards narrow the type properly? + if isinstance(payload, api_models_generated.JobResponseRRQueued): self._jobs[job_id] = JobQueued() - elif response["status"] == "ongoing": + elif isinstance(payload, api_models_generated.JobResponseRROngoing): self._jobs[job_id] = JobOngoing() - elif response["status"] == "cancelled": + elif isinstance(payload, api_models_generated.JobResponseRRFinished): + self._jobs[job_id] = JobFinished( + samples=[[state.__root__ for state in shot] for shot in payload.response.result] + ) + elif isinstance(payload, api_models_generated.JobResponseRRError): + self._jobs[job_id] = JobFailed(error=payload.response.message) + elif isinstance(payload, api_models_generated.JobResponseRRCancelled): self._jobs[job_id] = JobCancelled() - else: - raise RuntimeError(f"API returned unknown job status: {response['status']}.") + else: # pragma: no cover + assert_never(payload) def _aggregate_status(self) -> JobStatus: """Aggregate the Qiskit job status from the status of the individual circuit evaluations.""" diff --git a/qiskit_aqt_provider/aqt_resource.py b/qiskit_aqt_provider/aqt_resource.py index 8efe01b..165c751 100644 --- a/qiskit_aqt_provider/aqt_resource.py +++ b/qiskit_aqt_provider/aqt_resource.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. import abc -import typing import warnings from typing import Any, Dict, List, Type, TypeVar, Union +from uuid import UUID import httpx from qiskit import QuantumCircuit @@ -27,8 +27,9 @@ from qiskit_aer import AerJob, AerSimulator from typing_extensions import TypedDict +from qiskit_aqt_provider import api_models from qiskit_aqt_provider.aqt_job import AQTJob -from qiskit_aqt_provider.circuit_to_aqt import circuit_to_aqt +from qiskit_aqt_provider.circuit_to_aqt import circuit_to_aqt_job from qiskit_aqt_provider.constants import REQUESTS_TIMEOUT @@ -155,7 +156,7 @@ def __init__(self, provider: Provider, workspace: str, resource: ApiResource): self.options.set_validator("query_timeout_seconds", OptionalFloat) self.options.set_validator("query_period_seconds", Float) - def submit(self, circuit: QuantumCircuit, shots: int) -> str: + def submit(self, circuit: QuantumCircuit, shots: int) -> UUID: """Submit a circuit. Parameters: @@ -165,24 +166,14 @@ def submit(self, circuit: QuantumCircuit, shots: int) -> str: Returns: The unique identifier for the submitted job. """ - payload = circuit_to_aqt(circuit, shots=shots) + payload = circuit_to_aqt_job(circuit, shots=shots) url = f"{self.url}/submit/{self._workspace}/{self._resource['id']}" - req = httpx.post(url, json=payload, headers=self.headers, timeout=REQUESTS_TIMEOUT) + req = httpx.post(url, json=payload.json(), headers=self.headers, timeout=REQUESTS_TIMEOUT) req.raise_for_status() - response = req.json() + return api_models.Response.parse_raw(req.json()).job.job_id - api_job = response.get("job") - if api_job is None: - raise ValueError("API response does not contain field 'job'.") - - job_id = api_job.get("job_id") - if job_id is None: - raise ValueError("API response does not contain field 'job.job_id'.") - - return str(job_id) - - def result(self, job_id: str) -> Dict[str, Any]: + def result(self, job_id: UUID) -> api_models.JobResponse: """Query the result for a specific job. Parameters: @@ -194,7 +185,7 @@ def result(self, job_id: str) -> Dict[str, Any]: url = f"{self.url}/result/{job_id}" req = httpx.get(url, headers=self.headers, timeout=REQUESTS_TIMEOUT) req.raise_for_status() - return typing.cast(Dict[str, Any], req.json()) + return api_models.Response.parse_raw(req.json()) def configuration(self) -> BackendConfiguration: warnings.warn( @@ -306,15 +297,16 @@ def __init__(self, provider: Provider, workspace: str, resource: ApiResource) -> # TODO: also support a noisy simulator super().__init__(provider, workspace, resource) - self.jobs: Dict[str, AerJob] = {} + self.jobs: Dict[UUID, AerJob] = {} self.simulator = AerSimulator(method="statevector") - def submit(self, circuit: QuantumCircuit, shots: int) -> str: + def submit(self, circuit: QuantumCircuit, shots: int) -> UUID: job = self.simulator.run(circuit, shots=shots) - self.jobs[job.job_id()] = job - return str(job.job_id()) + job_id = UUID(hex=job.job_id()) + self.jobs[job_id] = job + return job_id - def result(self, job_id: str) -> Dict[str, Any]: + def result(self, job_id: UUID) -> api_models.JobResponse: qiskit_result = self.jobs[job_id].result() counts = qiskit_result.data()["counts"] num_qubits = qiskit_result.results[0].header.n_qubits @@ -323,16 +315,10 @@ def result(self, job_id: str) -> Dict[str, Any]: samples.extend( [qubit_states_from_int(int(hex_state, 16), num_qubits) for _ in range(occurences)] ) - return { - "job": { - "job_type": "quantum_circuit", - "label": "qiskit", - "job_id": job_id, - "resource_id": self._resource["id"], - "workspace_id": self._workspace, - }, - "response": { - "status": "finished", - "result": samples, - }, - } + + return api_models.Response.finished( + job_id=job_id, + workspace_id=self._workspace, + resource_id=self._resource["id"], + samples=samples, + ) diff --git a/qiskit_aqt_provider/circuit_to_aqt.py b/qiskit_aqt_provider/circuit_to_aqt.py index 1f1698f..45afad2 100644 --- a/qiskit_aqt_provider/circuit_to_aqt.py +++ b/qiskit_aqt_provider/circuit_to_aqt.py @@ -10,27 +10,30 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -from typing import Any, Dict, List +from typing import List from numpy import pi from qiskit import QuantumCircuit +from qiskit_aqt_provider import api_models -def _qiskit_to_aqt_circuit(circuit: QuantumCircuit) -> List[Dict[str, Any]]: + +def _qiskit_to_aqt_circuit(circuit: QuantumCircuit) -> api_models.Circuit: """Convert a Qiskit `QuantumCircuit` into a payload for AQT's quantum_circuit job type. Args: circuit: Qiskit circuit to convert. Returns: - list of instructions for AQT's quantum_circuit job type. + AQT API circuit payload. """ count = 0 qubit_map = {} for bit in circuit.qubits: qubit_map[bit] = count count += 1 - ops = [] + ops: List[api_models.OperationModel] = [] + num_measurements = 0 for instruction in circuit.data: @@ -44,28 +47,25 @@ def _qiskit_to_aqt_circuit(circuit: QuantumCircuit) -> List[Dict[str, Any]]: if inst.name == "rz": ops.append( - { - "operation": "RZ", - "phi": float(inst.params[0]) / pi, - "qubit": qubits[0], - } + api_models.Operation.rz( + phi=float(inst.params[0]) / pi, + qubit=qubits[0], + ) ) elif inst.name == "r": ops.append( - { - "operation": "R", - "phi": float(inst.params[1]) / pi, - "theta": float(inst.params[0]) / pi, - "qubit": qubits[0], - } + api_models.Operation.r( + phi=float(inst.params[1]) / pi, + theta=float(inst.params[0]) / pi, + qubit=qubits[0], + ) ) elif inst.name == "rxx": ops.append( - { - "operation": "RXX", - "theta": float(inst.params[0]) / pi, - "qubits": qubits[:2], - } + api_models.Operation.rxx( + theta=float(inst.params[0]) / pi, + qubits=qubits[:2], + ) ) elif inst.name == "measure": num_measurements += 1 @@ -77,11 +77,11 @@ def _qiskit_to_aqt_circuit(circuit: QuantumCircuit) -> List[Dict[str, Any]]: if not num_measurements: raise ValueError("Circuit must have at least one measurement operation.") - ops.append({"operation": "MEASURE"}) - return ops + ops.append(api_models.Operation.measure()) + return api_models.Circuit(__root__=ops) -def circuit_to_aqt(circuit: QuantumCircuit, shots: int) -> Dict[str, Any]: +def circuit_to_aqt_job(circuit: QuantumCircuit, shots: int) -> api_models.JobSubmission: """Convert a Qiskit circuit to its JSON representation for the AQT API. Args: @@ -91,14 +91,14 @@ def circuit_to_aqt(circuit: QuantumCircuit, shots: int) -> Dict[str, Any]: Returns: The corresponding circuit execution request payload. """ - seqs = _qiskit_to_aqt_circuit(circuit) - - return { - "job_type": "quantum_circuit", - "label": "qiskit", - "payload": { - "repetitions": shots, - "quantum_circuit": seqs, - "number_of_qubits": circuit.num_qubits, - }, - } + circuit_payload = _qiskit_to_aqt_circuit(circuit) + + return api_models.JobSubmission( + job_type="quantum_circuit", + label="qiskit", + payload=api_models.QuantumCircuit( + repetitions=shots, + quantum_circuit=circuit_payload, + number_of_qubits=circuit.num_qubits, + ), + ) diff --git a/qiskit_aqt_provider/test/fixtures.py b/qiskit_aqt_provider/test/fixtures.py index 93685d5..06a7f02 100644 --- a/qiskit_aqt_provider/test/fixtures.py +++ b/qiskit_aqt_provider/test/fixtures.py @@ -15,6 +15,7 @@ This module is exposed as pytest plugin for this project. """ +import uuid from typing import List, Tuple import pytest @@ -22,7 +23,7 @@ from qiskit_aqt_provider.aqt_provider import AQTProvider from qiskit_aqt_provider.aqt_resource import ApiResource, OfflineSimulatorResource -from qiskit_aqt_provider.circuit_to_aqt import circuit_to_aqt +from qiskit_aqt_provider.circuit_to_aqt import circuit_to_aqt_job class MockSimulator(OfflineSimulatorResource): @@ -37,7 +38,7 @@ def __init__(self) -> None: self.submit_call_args: List[Tuple[QuantumCircuit, int]] = [] - def submit(self, circuit: QuantumCircuit, shots: int) -> str: + def submit(self, circuit: QuantumCircuit, shots: int) -> uuid.UUID: """Submit the circuit for shots executions on the backend. Record the passed arguments in `submit_call_args`. @@ -52,7 +53,7 @@ def submit(self, circuit: QuantumCircuit, shots: int) -> str: ValueError: the circuit cannot be converted to the AQT JSON wire format. """ try: - _ = circuit_to_aqt(circuit, shots=shots) + _ = circuit_to_aqt_job(circuit, shots=shots) except Exception as e: # noqa: BLE001 raise ValueError("Circuit cannot be converted to AQT JSON format:\n{circuit}") from e diff --git a/qiskit_aqt_provider/test/resources.py b/qiskit_aqt_provider/test/resources.py index f5e850d..c9d9395 100644 --- a/qiskit_aqt_provider/test/resources.py +++ b/qiskit_aqt_provider/test/resources.py @@ -17,10 +17,11 @@ import time import uuid from dataclasses import dataclass, field -from typing import Any, Dict, List +from typing import Dict, List from qiskit import QuantumCircuit +from qiskit_aqt_provider import api_models from qiskit_aqt_provider.aqt_provider import AQTProvider from qiskit_aqt_provider.aqt_resource import ApiResource, AQTResource @@ -42,7 +43,7 @@ class TestJob: # pylint: disable=too-many-instance-attributes circuit: QuantumCircuit shots: int status: JobStatus = JobStatus.QUEUED - job_id: str = field(default_factory=lambda: str(uuid.uuid4())) + job_id: uuid.UUID = field(default_factory=lambda: uuid.uuid4()) time_queued: float = field(default_factory=time.time) time_submitted: float = 0.0 time_finished: float = 0.0 @@ -51,6 +52,9 @@ class TestJob: # pylint: disable=too-many-instance-attributes num_clbits: int = field(init=False) samples: List[List[int]] = field(init=False) + workspace: str = field(default="test-workspace", init=False) + resource: str = field(default="test-resource", init=False) + def __post_init__(self) -> None: """Calculate derived quantities.""" self.num_clbits = self.circuit.num_clbits @@ -76,22 +80,42 @@ def cancel(self) -> None: self.time_finished = time.time() self.status = JobStatus.CANCELLED - def response_payload(self) -> Dict[str, Any]: + def response_payload(self) -> api_models.JobResponse: """AQT API-compatible response for the current job status.""" if self.status is JobStatus.QUEUED: - return {"response": {"status": "queued"}} + return api_models.Response.queued( + job_id=self.job_id, + workspace_id=self.workspace, + resource_id=self.resource, + ) if self.status is JobStatus.ONGOING: - return {"response": {"status": "ongoing"}} + return api_models.Response.ongoing( + job_id=self.job_id, + workspace_id=self.workspace, + resource_id=self.resource, + ) if self.status is JobStatus.FINISHED: - return {"response": {"status": "finished", "result": self.samples}} + return api_models.Response.finished( + job_id=self.job_id, + workspace_id=self.workspace, + resource_id=self.resource, + samples=self.samples, + ) if self.status is JobStatus.ERROR: - return {"response": {"status": "error", "message": self.error_message}} + return api_models.Response.error( + job_id=self.job_id, + workspace_id=self.workspace, + resource_id=self.resource, + message=self.error_message, + ) if self.status is JobStatus.CANCELLED: - return {"response": {"status": "cancelled"}} + return api_models.Response.cancelled( + job_id=self.job_id, workspace_id=self.workspace, resource_id=self.resource + ) assert False, "unreachable" # pragma: no cover # noqa: PT015,S101 @@ -106,8 +130,6 @@ def __init__( *, min_queued_duration: float = 0.0, min_running_duration: float = 0.0, - always_invalid: bool = False, - always_invalid_status: bool = False, always_cancel: bool = False, always_error: bool = False, error_message: str = "", @@ -117,28 +139,24 @@ def __init__( Args: min_queued_duration: minimum time in seconds spent by all jobs in the QUEUED state min_running_duration: minimum time in seconds spent by all jobs in the ONGOING state - always_invalid: always return invalid payloads when queried - always_invalid_status: always return a valid payload but with an invalid status always_cancel: always cancel the jobs directly after submission always_error: always finish execution with an error error_message: the error message returned by failed jobs. Implies `always_error`. """ super().__init__( AQTProvider(""), - "test-resource", + "test-workspace", ApiResource(name="test-resource", id="test", type="simulator"), ) - self.jobs: Dict[str, TestJob] = {} + self.jobs: Dict[uuid.UUID, TestJob] = {} self.min_queued_duration = min_queued_duration self.min_running_duration = min_running_duration - self.always_invalid = always_invalid - self.always_invalid_status = always_invalid_status self.always_cancel = always_cancel self.always_error = always_error or error_message self.error_message = error_message or str(uuid.uuid4()) - def submit(self, circuit: QuantumCircuit, shots: int) -> str: + def submit(self, circuit: QuantumCircuit, shots: int) -> uuid.UUID: job = TestJob(circuit, shots, error_message=self.error_message) if self.always_cancel: @@ -147,16 +165,10 @@ def submit(self, circuit: QuantumCircuit, shots: int) -> str: self.jobs[job.job_id] = job return job.job_id - def result(self, job_id: str) -> Dict[str, Any]: + def result(self, job_id: uuid.UUID) -> api_models.JobResponse: job = self.jobs[job_id] now = time.time() - if self.always_invalid: - return {"invalid": "invalid"} - - if self.always_invalid_status: - return {"response": {"status": "invalid"}} - if job.status is JobStatus.QUEUED and (now - job.time_queued) > self.min_queued_duration: job.submit() diff --git a/scripts/api_models.py b/scripts/api_models.py new file mode 100755 index 0000000..7f03603 --- /dev/null +++ b/scripts/api_models.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# This code is part of Qiskit. +# +# (C) Copyright Alpine Quantum Technologies GmbH 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +import shlex +import subprocess +import sys +import tempfile +from functools import lru_cache +from pathlib import Path + +import typer + +app = typer.Typer() + + +@lru_cache +def repo_root() -> Path: + """Absolute path to the repository root.""" + return Path( + subprocess.run( + shlex.split("git rev-parse --show-toplevel"), # noqa: S603 + capture_output=True, + check=True, + ) + .stdout.strip() + .decode("utf-8") + ).absolute() + + +def default_schema_path() -> Path: + """Default location of the API schema definition.""" + return repo_root() / "api" / "aqt_public.yml" + + +def default_models_path() -> Path: + """Default destination of generated Pydantic models.""" + return repo_root() / "qiskit_aqt_provider" / "api_models_generated.py" + + +def generate_models(schema_path: Path) -> str: + """Generate Pydantic models from a given schema. + + Args: + schema_path: path to the file that contains the schema. + + Returns: + Source code of the Pydantic models. + """ + proc = subprocess.run( + shlex.split(f"datamodel-codegen --input {schema_path}"), # noqa: S603 + capture_output=True, + check=True, + ) + + return proc.stdout.decode() + + +@app.command() +def generate( + schema_path: Path = typer.Argument(default_schema_path), + models_path: Path = typer.Argument(default_models_path), +) -> None: + """Generate Pydantic models from a schema. + + Any existing content in `models_path` is will be lost! + + Args: + schema_path: path to the file that contains the schema + models_path: path of the file to write the generated models to. + """ + models_path.write_text(generate_models(schema_path)) + + +@app.command() +def check( + schema_path: Path = typer.Argument(default_schema_path), + models_path: Path = typer.Argument(default_models_path), +) -> None: + """Check if the Python models in `models_path` match the schema in `schema_path`. + + For the check to succeed, `models_path` must contain exactly what the + generator produces with `schema_path` as input. + """ + with tempfile.NamedTemporaryFile(mode="w") as reference_models: + filepath = Path(reference_models.name) + filepath.write_text(generate_models(schema_path)) + + proc = subprocess.run( + shlex.split(f"diff -u {filepath} {models_path}"), # noqa: S603 + capture_output=True, + ) + + if proc.returncode != 0: + print(proc.stdout.decode()) + sys.exit(proc.returncode) + + print("OK") + + +if __name__ == "__main__": + app() diff --git a/test/test_circuit_to_aqt.py b/test/test_circuit_to_aqt.py index a29d4d6..230c7ee 100644 --- a/test/test_circuit_to_aqt.py +++ b/test/test_circuit_to_aqt.py @@ -16,14 +16,15 @@ import pytest from qiskit import QuantumCircuit -from qiskit_aqt_provider.circuit_to_aqt import circuit_to_aqt +from qiskit_aqt_provider import api_models +from qiskit_aqt_provider.circuit_to_aqt import circuit_to_aqt_job def test_empty_circuit() -> None: """Circuits need at least one measurement operation.""" qc = QuantumCircuit(1) with pytest.raises(ValueError): - circuit_to_aqt(qc, shots=1) + circuit_to_aqt_job(qc, shots=1) def test_just_measure_circuit() -> None: @@ -33,17 +34,17 @@ def test_just_measure_circuit() -> None: qc = QuantumCircuit(1) qc.measure_all() - expected = { - "job_type": "quantum_circuit", - "label": "qiskit", - "payload": { - "quantum_circuit": [{"operation": "MEASURE"}], - "repetitions": shots, - "number_of_qubits": 1, - }, - } + expected = api_models.JobSubmission( + job_type="quantum_circuit", + label="qiskit", + payload=api_models.QuantumCircuit( + repetitions=shots, + number_of_qubits=1, + quantum_circuit=api_models.Circuit(__root__=[api_models.Operation.measure()]), + ), + ) - result = circuit_to_aqt(qc, shots=shots) + result = circuit_to_aqt_job(qc, shots=shots) assert result == expected @@ -56,37 +57,24 @@ def test_valid_circuit() -> None: qc.rxx(pi / 2, 0, 1) qc.measure_all() - result = circuit_to_aqt(qc, shots=1) - - expected = { - "job_type": "quantum_circuit", - "label": "qiskit", - "payload": { - "number_of_qubits": 2, - "repetitions": 1, - "quantum_circuit": [ - { - "operation": "R", - "theta": 0.5, - "phi": 0.0, - "qubit": 0, - }, - { - "operation": "RZ", - "phi": 0.2, - "qubit": 1, - }, - { - "operation": "RXX", - "theta": 0.5, - "qubits": [0, 1], - }, - { - "operation": "MEASURE", - }, - ], - }, - } + result = circuit_to_aqt_job(qc, shots=1) + + expected = api_models.JobSubmission( + job_type="quantum_circuit", + label="qiskit", + payload=api_models.QuantumCircuit( + number_of_qubits=2, + repetitions=1, + quantum_circuit=api_models.Circuit( + __root__=[ + api_models.Operation.r(theta=0.5, phi=0.0, qubit=0), + api_models.Operation.rz(phi=0.2, qubit=1), + api_models.Operation.rxx(theta=0.5, qubits=[0, 1]), + api_models.Operation.measure(), + ] + ), + ), + ) assert result == expected @@ -100,7 +88,7 @@ def test_invalid_gates_in_circuit() -> None: qc.measure_all() with pytest.raises(ValueError): - circuit_to_aqt(qc, shots=1) + circuit_to_aqt_job(qc, shots=1) def test_invalid_measurements() -> None: @@ -112,7 +100,7 @@ def test_invalid_measurements() -> None: qc_invalid.measure([1], [1]) with pytest.raises(ValueError): - circuit_to_aqt(qc_invalid, shots=1) + circuit_to_aqt_job(qc_invalid, shots=1) # same circuit as above, but with the measurements at the end is valid qc = QuantumCircuit(2, 2) @@ -121,31 +109,21 @@ def test_invalid_measurements() -> None: qc.measure([0], [0]) qc.measure([1], [1]) - result = circuit_to_aqt(qc, shots=1) - expected = { - "job_type": "quantum_circuit", - "label": "qiskit", - "payload": { - "number_of_qubits": 2, - "repetitions": 1, - "quantum_circuit": [ - { - "operation": "R", - "theta": 0.5, - "phi": 0.0, - "qubit": 0, - }, - { - "operation": "R", - "theta": 0.5, - "phi": 0.0, - "qubit": 1, - }, - { - "operation": "MEASURE", - }, - ], - }, - } + result = circuit_to_aqt_job(qc, shots=1) + expected = api_models.JobSubmission( + job_type="quantum_circuit", + label="qiskit", + payload=api_models.QuantumCircuit( + number_of_qubits=2, + repetitions=1, + quantum_circuit=api_models.Circuit( + __root__=[ + api_models.Operation.r(theta=0.5, phi=0.0, qubit=0), + api_models.Operation.r(theta=0.5, phi=0.0, qubit=1), + api_models.Operation.measure(), + ] + ), + ), + ) assert result == expected diff --git a/test/test_execution.py b/test/test_execution.py index 74f7638..8789d7f 100644 --- a/test/test_execution.py +++ b/test/test_execution.py @@ -100,38 +100,6 @@ def test_cancelled_circuit() -> None: assert result.success is False -def test_non_compliant_resource() -> None: - """Check that if the resource sends back ill-formed payloads, the job raises a - RuntimeError. - """ - backend = TestResource(always_invalid=True) - backend.options.update_options(query_period_seconds=0.1) - - qc = QuantumCircuit(1) - qc.measure_all() - - with pytest.raises(RuntimeError) as excinfo: - qiskit.execute(qc, backend).result() - - assert "Unexpected error while retrieving job status" in str(excinfo) - - -def test_non_compliant_resource_invalid_status() -> None: - """Check that if the resource sends back a valid payload with an invalid status - name, the job raises a RuntimeError. - """ - backend = TestResource(always_invalid_status=True) - backend.options.update_options(query_period_seconds=0.1) - - qc = QuantumCircuit(1) - qc.measure_all() - - with pytest.raises(RuntimeError) as excinfo: - qiskit.execute(qc, backend).result() - - assert "Unexpected error while retrieving job status" in str(excinfo) - - @pytest.mark.parametrize("shots", [1, 100, 200]) def test_simple_backend_run(shots: int, offline_simulator_no_noise: AQTResource) -> None: """Run a simple circuit with `backend.run`.""" diff --git a/test/test_resource.py b/test/test_resource.py index e8e7561..178affc 100644 --- a/test/test_resource.py +++ b/test/test_resource.py @@ -20,6 +20,7 @@ from qiskit import QuantumCircuit from qiskit.providers.exceptions import JobTimeoutError +from qiskit_aqt_provider import api_models from qiskit_aqt_provider.aqt_job import AQTJob from qiskit_aqt_provider.aqt_provider import AQTProvider from qiskit_aqt_provider.aqt_resource import ( @@ -146,7 +147,7 @@ def test_submit_valid_response(httpx_mock: HTTPXMock) -> None: """ token = str(uuid.uuid4()) backend = DummyResource(token) - expected_job_id = str(uuid.uuid4()) + expected_job_id = uuid.uuid4() def handle_submit(request: httpx.Request) -> httpx.Response: assert request.headers["sdk"] == "qiskit" @@ -154,16 +155,11 @@ def handle_submit(request: httpx.Request) -> httpx.Response: return httpx.Response( status_code=httpx.codes.OK, - json={ - "job": { - "job_id": expected_job_id, - "job_type": "quantum_circuit", - "label": "Example computation", - "resource_id": backend._resource["id"], - "workspace_id": backend._workspace, - }, - "response": {"status": "queued"}, - }, + json=api_models.Response.queued( + job_id=expected_job_id, + resource_id=backend._resource["id"], + workspace_id=backend._workspace, + ).json(), ) httpx_mock.add_callback(handle_submit, method="POST") @@ -183,52 +179,23 @@ def test_submit_bad_request(httpx_mock: HTTPXMock) -> None: backend.submit(empty_circuit(2), shots=10) -def test_submit_bad_payload_no_job(httpx_mock: HTTPXMock) -> None: - """Check that AQTResource.submit raises a ValueError if the returned - payload does not contain a job field. - """ - backend = DummyResource("") - httpx_mock.add_response(json={}) - - with pytest.raises(ValueError, match=r"^API response does not contain field"): - backend.submit(empty_circuit(2), shots=10) - - -def test_submit_bad_payload_no_jobid(httpx_mock: HTTPXMock) -> None: - """Check that AQTResource.submit raises a ValueError if the returned - payload does not contain a job.job_id field. - """ - backend = DummyResource("") - httpx_mock.add_response(json={"job": {}}) - - with pytest.raises(ValueError, match=r"^API response does not contain field"): - backend.submit(empty_circuit(2), shots=10) - - def test_result_valid_response(httpx_mock: HTTPXMock) -> None: """Check that AQTResource.result passes the authorization token and returns the raw response payload. """ token = str(uuid.uuid4()) backend = DummyResource(token) - job_id = str(uuid.uuid4()) - - payload = { - "job": { - "job_id": job_id, - "job_type": "quantum_circuit", - "label": "Example computation", - "resource_id": backend._resource["id"], - "workspace_id": backend._workspace, - }, - "response": {"status": "cancelled"}, - } + job_id = uuid.uuid4() + + payload = api_models.Response.cancelled( + job_id=job_id, resource_id=backend._resource["id"], workspace_id=backend._workspace + ) def handle_result(request: httpx.Request) -> httpx.Response: assert request.headers["sdk"] == "qiskit" assert request.headers["authorization"] == f"Bearer {token}" - return httpx.Response(status_code=httpx.codes.OK, json=payload) + return httpx.Response(status_code=httpx.codes.OK, json=payload.json()) httpx_mock.add_callback(handle_result, method="GET") @@ -244,7 +211,20 @@ def test_result_bad_request(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(status_code=httpx.codes.BAD_REQUEST) with pytest.raises(httpx.HTTPError): - backend.result(str(uuid.uuid4())) + backend.result(uuid.uuid4()) + + +def test_result_unknown_job(httpx_mock: HTTPXMock) -> None: + """Check that AQTResource.result raises UnknownJobError if the API + responds with an UnknownJob payload. + """ + backend = DummyResource("") + job_id = uuid.uuid4() + + httpx_mock.add_response(json=api_models.Response.unknown_job(job_id=job_id).json()) + + with pytest.raises(api_models.UnknownJobError, match=str(job_id)): + backend.result(job_id) def test_resource_fixture_detect_invalid_circuits(