diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml deleted file mode 100644 index 76dbeb9da..000000000 --- a/.github/workflows/build-docker-image.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: build-docker-image - -on: - workflow_dispatch: - inputs: - tag: - description: 'Tag for Docker image' - required: true - ref: - description: 'Branch, tag, or commit SHA to build from' - required: true - default: main - -jobs: - build-and-push: - runs-on: ubuntu-latest - timeout-minutes: 300 - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.ref }} - - - name: Log in to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v2.10.0 - with: - context: . - push: true - tags: octue/octue-sdk-python:${{ github.event.inputs.tag }} diff --git a/.gitignore b/.gitignore index dfd8963eb..2ba81f335 100644 --- a/.gitignore +++ b/.gitignore @@ -98,7 +98,7 @@ ENV/ # See: https://github.com/google-github-actions/auth/issues/123 google_credentials.json gha-creds-*.json -gcp-creds-*.json +gcp-cred* terraform/gcp-credentials.json .terraform diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26e4dc519..b1980e11c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,22 +13,24 @@ repos: - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.9.2 hooks: - id: ruff + name: Ruff lint args: [--fix, --exit-non-zero-on-fix] + + - id: ruff + name: Ruff isort + args: [check, --select, I, --fix] + - id: ruff-format + name: Ruff format - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.7.1 hooks: - id: prettier - - repo: https://github.com/pycqa/pydocstyle - rev: 6.1.1 - hooks: - - id: pydocstyle - - repo: https://github.com/thclark/pre-commit-sphinx rev: 0.0.3 hooks: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1a1db8307..000000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM python:3.8.12-slim - -RUN apt-get update && apt-get install -y git curl - -ENV POETRY_HOME=/etc/poetry -RUN curl -sSL https://install.python-poetry.org | python3 - -ENV PATH="$POETRY_HOME/bin:$PATH" -RUN poetry config virtualenvs.create false - -# Install python dependencies. Note that poetry installs any root packages by default, but this is not available at this -# stage of caching dependencies. So we do a dependency-only install here to cache the dependencies, then a full poetry -# install post-create to install the root package, which will change more rapidly than dependencies. -COPY pyproject.toml poetry.lock ./ -RUN poetry install --no-ansi --no-interaction --only=main --no-root -v - -COPY . . -RUN poetry install --no-ansi --no-interaction --only=main -v diff --git a/docs/source/asking_questions.rst b/docs/source/asking_questions.rst index 4c00e45c2..1f579c4be 100644 --- a/docs/source/asking_questions.rst +++ b/docs/source/asking_questions.rst @@ -63,14 +63,18 @@ You can also set the following options when you call :mod:`Child.ask `) +- ``asynchronous`` - if true, don't wait for an answer to the question (the result and other events can be :ref:`retrieved from the event store later `) +- ``cpus`` - the number of CPUs to request for the question; defaults to the number set by the child service +- ``memory`` - the amount of memory to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service +- ``ephemeral_storage`` - the amount of ephemeral storage to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service - ``timeout`` - how long in seconds to wait for an answer (``None`` by default - i.e. don't time out) If the question fails: + - If ``raise_errors=False``, the unraised error is returned - If ``raise_errors=False`` and ``max_retries > 0``, the question is retried up to this number of times - If ``raise_errors=False``, ``max_retries > 0``, and ``prevent_retries_when`` is a list of exception types, the question is retried unless the error type is in the list @@ -101,14 +105,12 @@ access the event store and run: from octue.cloud.pub_sub.bigquery import get_events - events = get_events( - table_id="your-project.your-dataset.your-table", - question_uuid="53353901-0b47-44e7-9da3-a3ed59990a71", - ) + events = get_events(question_uuid="53353901-0b47-44e7-9da3-a3ed59990a71") **Options** +- ``table_id`` - If you're not using the standard deployment, you can specify a different table here - ``question_uuid`` - Retrieve events from this specific question - ``parent_question_uuid`` - Retrieve events from questions triggered by the same parent question (this doesn't include the parent question's events) - ``originator_question_uuid`` - Retrieve events for the entire tree of questions triggered by an originator question (a question asked manually through ``Child.ask``; this does include the originator question's events) @@ -219,7 +221,7 @@ This method uses multithreading, allowing all the questions to be asked at once Asking a question within a service ================================== -If you have :doc:`created your own Octue service ` and want to ask children questions, you can do +If you have :doc:`created your own Twined service ` and want to ask children questions, you can do this more easily than above. Children are accessible from the ``analysis`` object by the keys you give them in the :ref:`app configuration ` file. For example, you can ask an ``elevation`` service a question like this: @@ -351,20 +353,17 @@ whole tree of children, grandchildren, and so on, please `upvote this issue. Using a service registry ======================== When asking a question, you can optionally specify one or more `service registries -`_ to resolve SRUIDs against. This is analogous to specifying a -different ``pip`` index for resolving package names when using ``pip install``. If you don't specify any registries, the -default Octue service registry is used. - -Specifying service registries can be useful if: +`_ to resolve SRUIDs against. This checks if the service revision +exists (good for catching typos in SRUIDs) and raises an error if it doesn't. Service registries can also get the +default revision of a service if you don't provide a revision tag. Asking a question if without specifying a registry +will bypass these checks. -- You have your own private services that aren't on the default Octue service registry -- You want services from one service registry with the same name as in another service registry to be prioritised Specifying service registries ----------------------------- You can specify service registries in two ways: -1. Globally for all questions asked inside a service. In the service configuration (``octue.yaml`` file): +1. For all questions asked inside a service. In the service configuration (``octue.yaml`` file): .. code-block:: yaml diff --git a/docs/source/authentication.rst b/docs/source/authentication.rst index aadedfe14..8c12f1450 100644 --- a/docs/source/authentication.rst +++ b/docs/source/authentication.rst @@ -4,42 +4,45 @@ Authentication You need authentication while using ``octue`` to: - Access data from Google Cloud Storage -- Use, run, or deploy Octue services +- Use, run, or deploy Twined services -Authentication can be provided by using one of: - -- A service account -- Application Default Credentials +Authentication is provided by a GCP service account. Creating a service account ========================== -1. Create a service account (see Google's `getting started guide `__) -2. Make sure your service account has access to any buckets you need, Google Pub/Sub, and Google Cloud Run if your - service is deployed on it (`see here `_) +By setting up your Twined service network with the :doc:`Twined Terraform modules `, a set of +maintainer service accounts have already been created with the required permissions. Using a service account ======================= Locally ------- -1. Create and download a key for your service account - it will be called ``your-project-XXXXX.json``. +1. Access your service accounts `here `_, making sure the + correct project is selected + +2. Click on the relevant service account, go to the "Keys" tab, and create (download) a JSON key for it - it will be + called ``-XXXXX.json``. .. DANGER:: It's best not to store this in your project to prevent accidentally committing it or building it into a docker - image layer. Instead, bind mount it into your docker image from somewhere else on your local system. + image layer. Instead, keep it somewhere else on your local system with any other service account keys you already + have. - If you must keep within your project, it's good practice to name the file ``gha-greds-.json`` and make - sure that ``gha-creds-*`` is in your ``.gitignore`` and ``.dockerignore`` files. + If you must keep within your project, it's good practice to name the file ``gcp-credentials.json`` and make + sure that ``gcp-cred*`` is in your ``.gitignore`` and ``.dockerignore`` files. -2. If you're developing in a container (like a VSCode ``.devcontainer``), mount the file into the container. You can +2. If you're developing in a container (like a VSCode ``devcontainer``), mount the file into the container. You can make gcloud available too - check out `this tutorial `_. -3. Set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to the path of the key file. +3. Set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to the absolute path of the key file. If using a + ``devcontainer``, make sure this is the path inside the container and not the path on your local machine. -On GCP infrastructure ---------------------- -- Credentials are provided when running code on GCP infrastructure (e.g. Google Cloud Run) -- ``octue`` uses these when when running on these platforms -- You should ensure the correct service account is being used by the deployed instance +On GCP infrastructure / deployed services +----------------------------------------- +- Credentials are automatically provided when running code or services on GCP infrastructure, including the Kubernetes + cluster +- ``octue`` uses these when when running on these platforms, so there's no need to upload a service account key or + include one in service docker images diff --git a/docs/source/bibliography.rst b/docs/source/bibliography.rst deleted file mode 100644 index ab2a29f71..000000000 --- a/docs/source/bibliography.rst +++ /dev/null @@ -1,128 +0,0 @@ -.. _sec-bibliography: - -============ -Bibliography -============ - -.. [Agarwal] S. Agarwal, N. Snavely, S. M. Seitz and R. Szeliski, - **Bundle Adjustment in the Large**, *Proceedings of the European - Conference on Computer Vision*, pp. 29--42, 2010. - -.. [Bjorck] A. Bjorck, **Numerical Methods for Least Squares - Problems**, SIAM, 1996 - -.. [Brown] D. C. Brown, **A solution to the general problem of - multiple station analytical stereo triangulation**, Technical - Report 43, Patrick Airforce Base, Florida, 1958. - -.. [ByrdNocedal] R. H. Byrd, J. Nocedal, R. B. Schanbel, - **Representations of Quasi-Newton Matrices and their use in Limited - Memory Methods**, *Mathematical Programming* 63(4):129–-156, 1994. - -.. [ByrdSchnabel] R.H. Byrd, R.B. Schnabel, and G.A. Shultz, **Approximate - solution of the trust region problem by minimization over - two dimensional subspaces**, *Mathematical programming*, - 40(1):247–263, 1988. - -.. [Chen] Y. Chen, T. A. Davis, W. W. Hager, and - S. Rajamanickam, **Algorithm 887: CHOLMOD, Supernodal Sparse - Cholesky Factorization and Update/Downdate**, *TOMS*, 35(3), 2008. - -.. [Conn] A.R. Conn, N.I.M. Gould, and P.L. Toint, **Trust region - methods**, *Society for Industrial Mathematics*, 2000. - -.. [GolubPereyra] G.H. Golub and V. Pereyra, **The differentiation of - pseudo-inverses and nonlinear least squares problems whose - variables separate**, *SIAM Journal on numerical analysis*, - 10(2):413–432, 1973. - -.. [HartleyZisserman] R.I. Hartley & A. Zisserman, **Multiview - Geometry in Computer Vision**, Cambridge University Press, 2004. - -.. [KanataniMorris] K. Kanatani and D. D. Morris, **Gauges and gauge - transformations for uncertainty description of geometric structure - with indeterminacy**, *IEEE Transactions on Information Theory* - 47(5):2017-2028, 2001. - -.. [Keys] R. G. Keys, **Cubic convolution interpolation for digital - image processing**, *IEEE Trans. on Acoustics, Speech, and Signal - Processing*, 29(6), 1981. - -.. [KushalAgarwal] A. Kushal and S. Agarwal, **Visibility based - preconditioning for bundle adjustment**, *In Proceedings of the - IEEE Conference on Computer Vision and Pattern Recognition*, 2012. - -.. [Kanzow] C. Kanzow, N. Yamashita and M. Fukushima, - **Levenberg–Marquardt methods with strong local convergence - properties for solving nonlinear equations with convex - constraints**, *Journal of Computational and Applied Mathematics*, - 177(2):375–397, 2005. - -.. [Levenberg] K. Levenberg, **A method for the solution of certain - nonlinear problems in least squares**, *Quart. Appl. Math*, - 2(2):164–168, 1944. - -.. [LiSaad] Na Li and Y. Saad, **MIQR: A multilevel incomplete qr - preconditioner for large sparse least squares problems**, *SIAM - Journal on Matrix Analysis and Applications*, 28(2):524–550, 2007. - -.. [Madsen] K. Madsen, H.B. Nielsen, and O. Tingleff, **Methods for - nonlinear least squares problems**, 2004. - -.. [Mandel] J. Mandel, **On block diagonal and Schur complement - preconditioning**, *Numer. Math.*, 58(1):79–93, 1990. - -.. [Marquardt] D.W. Marquardt, **An algorithm for least squares - estimation of nonlinear parameters**, *J. SIAM*, 11(2):431–441, - 1963. - -.. [Mathew] T.P.A. Mathew, **Domain decomposition methods for the - numerical solution of partial differential equations**, Springer - Verlag, 2008. - -.. [NashSofer] S.G. Nash and A. Sofer, **Assessing a search direction - within a truncated newton method**, *Operations Research Letters*, - 9(4):219–221, 1990. - -.. [Nocedal] J. Nocedal, **Updating Quasi-Newton Matrices with Limited - Storage**, *Mathematics of Computation*, 35(151): 773--782, 1980. - -.. [NocedalWright] J. Nocedal & S. Wright, **Numerical Optimization**, - Springer, 2004. - -.. [Oren] S. S. Oren, **Self-scaling Variable Metric (SSVM) Algorithms - Part II: Implementation and Experiments**, Management Science, - 20(5), 863-874, 1974. - -.. [Ridders] C. J. F. Ridders, **Accurate computation of F'(x) and - F'(x) F"(x)**, Advances in Engineering Software 4(2), 75-76, 1978. - -.. [RuheWedin] A. Ruhe and P.Å. Wedin, **Algorithms for separable - nonlinear least squares problems**, Siam Review, 22(3):318–337, - 1980. - -.. [Saad] Y. Saad, **Iterative methods for sparse linear - systems**, SIAM, 2003. - -.. [Stigler] S. M. Stigler, **Gauss and the invention of least - squares**, *The Annals of Statistics*, 9(3):465-474, 1981. - -.. [TenenbaumDirector] J. Tenenbaum & B. Director, **How Gauss - Determined the Orbit of Ceres**. - -.. [TrefethenBau] L.N. Trefethen and D. Bau, **Numerical Linear - Algebra**, SIAM, 1997. - -.. [Triggs] B. Triggs, P. F. Mclauchlan, R. I. Hartley & - A. W. Fitzgibbon, **Bundle Adjustment: A Modern Synthesis**, - Proceedings of the International Workshop on Vision Algorithms: - Theory and Practice, pp. 298-372, 1999. - -.. [Wiberg] T. Wiberg, **Computation of principal components when data - are missing**, In Proc. *Second Symp. Computational Statistics*, - pages 229–236, 1976. - -.. [WrightHolt] S. J. Wright and J. N. Holt, **An Inexact - Levenberg Marquardt Method for Large Sparse Nonlinear Least - Squares**, *Journal of the Australian Mathematical Society Series - B*, 26(4):387–403, 1985. diff --git a/docs/source/creating_apps.rst b/docs/source/creating_apps.rst index e68e00040..ba7dcdd72 100644 --- a/docs/source/creating_apps.rst +++ b/docs/source/creating_apps.rst @@ -58,7 +58,7 @@ Accessing inputs and storing outputs ------------------------------------ Your app must access configuration and input data from and store output data on the :mod:`analysis ` parameter (for function-based apps) or attribute (for class-based apps). This allows standardised -configuration/input/output validation against the twine and interoperability of all Octue services while leaving you +configuration/input/output validation against the twine and interoperability of all Twined services while leaving you freedom to do any kind of computation. To access the data, use the following attributes on the ``analysis`` parameter/attribute: diff --git a/docs/source/creating_services.rst b/docs/source/creating_services.rst index 3661745ec..34470d81a 100644 --- a/docs/source/creating_services.rst +++ b/docs/source/creating_services.rst @@ -9,9 +9,9 @@ return answers. They can run locally on any machine or be deployed to the cloud. - The language of the entrypoint must by ``python3`` (you can call processes using other languages within this though) -Anatomy of an Octue service +Anatomy of a Twined service =========================== -An Octue service is defined by the following files (located in the repository root by default). +A Twined service is defined by the following files (located in the repository root by default). app.py ------ @@ -52,7 +52,6 @@ octue.yaml - ``app_source_path: `` - if your ``app.py`` file is not in the repository root - ``app_configuration_path: `` - if your app needs an app configuration file that isn't in the repository root - - ``dockerfile_path: `` - if your app needs a ``Dockerfile`` that isn't in the repository root All paths should be relative to the repository root. Other valid entries can be found in the :mod:`ServiceConfiguration ` constructor. @@ -71,7 +70,7 @@ App configuration file (optional) ---- - If your app needs any configuration, asks questions to any other Octue services, or produces output + If your app needs any configuration, asks questions to any other Twined services, or produces output datafiles/datasets, you will need to provide an app configuration. Currently, this must take the form of a JSON file. It can contain the following keys: @@ -93,10 +92,10 @@ Dockerfile (optional) ---- - Octue services run in a Docker container if they are deployed. They can also run this way locally. The SDK + Twined services run in a Docker container if they are deployed. They can also run this way locally. The SDK provides a default ``Dockerfile`` for these purposes that will work for most cases: - - For deploying to `Google Cloud Run `_ + - For deploying to `Kubernetes `_ However, you may need to write and provide your own ``Dockerfile`` if your app requires: @@ -106,10 +105,11 @@ Dockerfile (optional) Here are two examples of a custom ``Dockerfile`` that use different base images: - - `A TurbSim service `_ - - `An OpenFAST service `_ + - `A TurbSim service `_ + - `An OpenFAST service `_ - If you do provide one, you must specify its path in ``octue.yaml`` under the ``dockerfile_path`` key. + If you do provide one, you must provide its path relative to your repository to the `build-twined-services` + GitHub Actions `workflow `_. As always, if you need help with this, feel free to drop us a message or raise an issue! @@ -149,7 +149,7 @@ Template apps We've created some template apps for you to look at and play around with. We recommend going through them in this order: 1. The `fractal app template `_ - - introduces a basic Octue service that returns output values to its parent. + introduces a basic Twined service that returns output values to its parent. 2. The `using-manifests app template `_ - introduces using a manifest of output datasets to return output files to its parent. 3. The `child-services app template `_ - @@ -160,14 +160,11 @@ Deploying services automatically ================================ Automated deployment with Octue means: -- Your service runs in Google Cloud, ready to accept questions from and return answers to other services. +- Your service runs in Google Kubernetes Engine (GKE), ready to accept questions from and return answers to other services. - You don't need to do anything to update your deployed service with new code changes - the service simply gets rebuilt and re-deployed each time you push a commit to your ``main`` branch, or merge a pull request into it (other branches and deployment strategies are available, but this is the default). - Serverless is the default - your service only runs when questions from other services are sent to it, meaning there - is no cost to having it deployed but not in use. + are minimal costs to having it deployed but not in use. -To enable automated deployments, contact us so we can create a Google Cloud Build trigger linked to your git repository. -This requires no work from you apart from authorising the connection to GitHub (or another git provider). - -If you want to deploy services yourself, see :doc:`here `. +If you'd like help deploying services, contact us. To do it yourself, see :doc:`here `. diff --git a/docs/source/datafile.rst b/docs/source/datafile.rst index d33213441..32c405987 100644 --- a/docs/source/datafile.rst +++ b/docs/source/datafile.rst @@ -21,7 +21,7 @@ Datafile Use a datafile to work with a file if you want to: - Read/write to local and cloud files in the same way - - Include it in a :doc:`dataset ` that can be sent to an Octue service for processing + - Include it in a :doc:`dataset ` that can be sent to a Twined service for processing - Add metadata to it for future sorting and filtering Key features diff --git a/docs/source/dataset.rst b/docs/source/dataset.rst index 1ad79450b..f7f4a686b 100644 --- a/docs/source/dataset.rst +++ b/docs/source/dataset.rst @@ -23,7 +23,7 @@ Dataset - Group together a set of files that naturally relate to each other e.g. a timeseries that's been split into multiple files. - Add metadata to it for future sorting and filtering - - Include it in a :doc:`manifest ` with other datasets and send them to an Octue service for processing + - Include it in a :doc:`manifest ` with other datasets and send them to a Twined service for processing Key features diff --git a/docs/source/deploying_services.rst b/docs/source/deploying_services.rst index fef8ee3fa..bbf913d02 100644 --- a/docs/source/deploying_services.rst +++ b/docs/source/deploying_services.rst @@ -3,96 +3,65 @@ ====================================== Deploying services (developer's guide) ====================================== -This is a guide for developers that want to deploy Octue services themselves - it is not needed if Octue manages your -services for you or if you are only asking questions to existing Octue services. +This is a guide for developers that want to deploy Twined services themselves - it is not needed if Octue manages your +services for you or if you are only asking questions to existing Twined services. -.. attention:: +What is deployment? +=================== +Deploying a Twined service means the service: - The ``octue deploy`` CLI command can be used to deploy services automatically, but it: +* Is a docker image that is spun up and down in a Kubernetes cluster on demand +* Is ready at any time to answer questions from users and other Twined services in the service network +* Can ask questions to any other Twined service in the service network +* Will automatically spin down after it has finished answering a question +* Will automatically build and redeploy after a relevant code change (e.g. on push or merge into ``main``) - - Is in alpha so may not work as intended - - Requires the ``gcloud`` CLI tool with ``Google Cloud SDK 367.0.0`` and ``beta 2021.12.10`` to be available - - Requires the correct permissions via the ``gcloud`` tool logged into a Google user account and/or with an - appropriate service account available +We can split deployment into service deployment and infrastructure deployment. - For now, we recommend `contacting us `_ to help set up deployments for you. +Deploying a service +=================== +Assuming the service network infrastructure already exists, a service can be deployed by building and pushing its docker +image to the service network's Artifact Registry repository. We recommend pushing a new image for each release of the +code e.g. on merge into the ``main`` branch. Each new image is the deployment of a new service revision. This can be +done automatically: -What deployment enables ------------------------ -Deploying an Octue service to Google Cloud Run means it: +- Follow the `instructions `_ + to add the `build-twined-service `_ + GitHub Actions workflow to your service's GitHub repository. Set its trigger to merge or push to ``main`` (see example + below) +- This needs to be done **once for every service** you want to deploy +- A live example can be `found here `_ + including automated pre-deployment testing and creation of a GitHub release -* Is deployed as a docker container -* Is ready to be asked questions by any other Octue service that has the correct permissions (you can control this) -* Can ask questions to any other Octue service for which it has the correct permissions -* Will automatically build and redeploy upon the conditions you provide (e.g. pushes or merges into ``main``) -* Will automatically start and run when Pub/Sub messages are received from the topic you created. The Pub/Sub - messages can be sent from anywhere in the world, but the container will only run in the region you chose (you can - create multiple Cloud Run services in different regions for the same repository if this is a problem). -* Will automatically stop shortly after finishing the analyses asked for in the Pub/Sub message (although - you can set a minimum container count so one is always running to minimise cold starts). +You can now :doc:`ask your service some questions `! It will be available in the service network as +``/:`` (e.g. ``octue/example-service-kueue:0.1.1``). -How to deploy -------------- -1. Ensuring you are in the desired project, go to the `Google Cloud Run `_ page - and create a new service - -.. image:: images/deploying_services_advanced/create_service.png - -2. Give your service a unique name - -.. image:: images/deploying_services_advanced/service_name_and_region.png - -3. Choose a `low-carbon region `_ that supports Eventarc - triggers and is in a convenient geographic location for you (e.g. physically close to you for low latency or in a - region compatible with your data protection requirements). - -.. image:: images/deploying_services_advanced/low_carbon_regions.png - -3. Click "Next". When changes are made to the source code, we want them to be deployed automatically. So, we need to - connect the repository to GCP to enable this. Select "Continuously deploy new revisions from a source repository" and - then "Set up with cloud build". - -.. image:: images/deploying_services_advanced/set_up_with_cloud_build.png - -4. Choose your source code repository provider and the repository containing the code you'd like to deploy. You'll have - to give the provider permission to access the repository. If your provider isn't GitHub, BitBucket, or Google Cloud - Source Repositories (GCSR), you'll need to mirror the repository to GCSR before completing this step as Google Cloud - Build only supports these three providers currently. -.. image:: images/deploying_services_advanced/choose_repository.png +Deploying the infrastructure +============================ -5. Click "Next", enter a regular expression for the branches you want to automatically deploy from (``main`` by default). - As the service will run in a docker container, select "Dockerfile" and click "Save". - -.. image:: images/deploying_services_advanced/choose_dockerfile.png - -6. Click "Next". If you want your service to be private, select "Allow internal traffic only" and "Require - authentication". This stops anyone without permission from using the service. - -.. image:: images/deploying_services_advanced/set_traffic.png - -7. The service needs a trigger to start up and respond to. We'll be using Google Pub/Sub. Click "Add eventarc trigger", - choose "Cloud Pub/Sub topic" as the trigger event, click on the menu called "Select a Cloud Pub/Sub topic", then - click "Create a topic". Any services that want to ask your service a question will publish their question to this - topic. - -.. image:: images/deploying_services_advanced/create_trigger.png - -8. The topic ID should be in the form ``octue.services.my-organisation.my-service``. Click "Create topic". - -9. Under "Invocation settings", click on the "Service account" menu and then "Create new service account". +Prerequisites +------------- +Twined services are currently deployable to Google Cloud Platform (GCP). You must have "owner" level access to the GCP +project you're deploying to and billing must be set up for it. -.. image:: images/deploying_services_advanced/create_service_account.png +Deploying step-by-step +---------------------- +There are two steps to deploying the infrastructure: -10. Make a new service account with a related name e.g. "my-service", then click "Create". Add the - "octue-service-user" and "Cloud Run Invoker" roles to the service account. Contact us if the "octue-service-user" - role is not available. +1. Deploy the core infrastructure (storage bucket, event store, IAM service accounts and roles) +2. Deploy the Kubernetes cluster, event handler, service registry, and Pub/Sub topic -.. image:: images/deploying_services_advanced/add_roles_to_service_account.png +1. Deploy core infrastructure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -11. Click "Save" and then "Create". +- Follow `the instructions `_ to deploy the resources in the + ``terraform-octue-twined-core`` Terraform module +- This only needs to be done once per service network -.. image:: images/deploying_services_advanced/save_and_create.png +2. Deploy Kubernetes cluster +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -12. You can now view your service in the list of `Cloud Run services `_ and view - its build trigger in the list of `Cloud Build triggers `_. +- Follow the `instructions `_ to deploy the resources in the + ``terraform-octue-twined-cluster`` Terraform module +- This only needs to be done once per service network diff --git a/docs/source/index.rst b/docs/source/index.rst index dcb2e6b79..c5dffb873 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,16 +2,16 @@ Octue SDK (Python) ================== -The python SDK for `Octue `_ data services, digital twins, and applications - get faster data +The python SDK for `Octue `_ Twined scientific data services and digital twins - get faster data groundwork so you have more time for the science! .. _service_definition: .. admonition:: Definition - Octue service - An Octue data service, digital twin, or application that can be asked questions, process them, and return - answers. Octue services can communicate with each other with minimal extra setup. + Octue Twined service + A data service or digital twin built on the ``octue`` SDK that can be asked questions, process them, and return + answers. Twined services can communicate with each other with minimal extra setup. Key features @@ -40,7 +40,7 @@ Key features **Create, run, and deploy your apps as services** - No need to change your app - just wrap it -- Use the ``octue`` CLI to run your service locally or deploy it to Google Cloud Run +- Use the ``octue`` CLI to run your service locally or deploy it to Google Kubernetes Engine (GKE) - Create JSON-schema interfaces to explicitly define the form of configuration, input, and output data - Ask other services questions as part of your app (i.e. build trees of services) - Automatically display readable, colourised logs, or use your own log handler @@ -96,4 +96,3 @@ We use `GitHub Issues `_ [#]_ api license version_history - bibliography diff --git a/docs/source/inter_service_compatibility.rst b/docs/source/inter_service_compatibility.rst index df6280926..2170f4db4 100644 --- a/docs/source/inter_service_compatibility.rst +++ b/docs/source/inter_service_compatibility.rst @@ -2,122 +2,118 @@ Inter-service compatibility =========================== -Octue services acting as parents and children communicate with each other according to the `services communication -schema `_. Up until version ``0.51.0``, services running nearly -all versions of ``octue`` could communicate with each other compatibly. To allow a significant infrastructure upgrade, -version ``0.51.0`` introduced a number of breaking changes to the standard meaning services running versions ``0.51.0`` -to ``0.52.1`` are only able to communicate with other services running versions in the same range. The same applies to -services running versions ``>=0.53.0``. - -The table below shows which ``octue`` versions parents can run (rows) to send questions compatible with versions -children are running (columns). Note that this table does not display whether children's responses are compatible with -the parent, just that a child is able to accept a question. +Twined services acting as parents and children communicate with each other according to the `services communication +schema `_. The table below shows which ``octue`` versions parents +can run (rows) to send questions compatible with versions children are running (columns). Note that this table does not +display whether children's responses are compatible with the parent, just that a child is able to accept a question. **Key** - ``0`` = incompatible - ``1`` = compatible -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| | 0.61.2 | 0.61.1 | 0.61.0 | 0.60.2 | 0.60.1 | 0.60.0 | 0.59.1 | 0.59.0 | 0.58.0 | 0.57.2 | 0.57.1 | 0.57.0 | 0.56.0 | 0.55.0 | 0.54.0 | 0.53.0 | 0.52.2 | 0.52.1 | 0.52.0 | 0.51.0 | 0.50.1 | 0.50.0 | 0.49.2 | 0.49.1 | 0.49.0 | 0.48.0 | 0.47.2 | 0.47.1 | 0.47.0 | 0.46.3 | 0.46.2 | 0.46.1 | 0.46.0 | 0.45.0 | 0.44.0 | 0.43.7 | 0.43.6 | 0.43.5 | 0.43.4 | 0.43.3 | 0.43.2 | 0.43.1 | 0.43.0 | 0.42.1 | 0.42.0 | 0.41.1 | 0.41.0 | 0.40.2 | 0.40.1 | 0.40.0 | -+========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+ -| 0.61.2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.61.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.61.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.60.2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.60.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.60.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.59.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.59.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.58.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.57.2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.57.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.57.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.56.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.55.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.54.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.53.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.52.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.52.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.52.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.51.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.50.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.50.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.49.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.49.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.49.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.48.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.47.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.47.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.47.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.45.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.44.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.42.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.42.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.41.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.41.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.40.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.40.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.40.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| | 0.62.0 | 0.61.2 | 0.61.1 | 0.61.0 | 0.60.2 | 0.60.1 | 0.60.0 | 0.59.1 | 0.59.0 | 0.58.0 | 0.57.2 | 0.57.1 | 0.57.0 | 0.56.0 | 0.55.0 | 0.54.0 | 0.53.0 | 0.52.2 | 0.52.1 | 0.52.0 | 0.51.0 | 0.50.1 | 0.50.0 | 0.49.2 | 0.49.1 | 0.49.0 | 0.48.0 | 0.47.2 | 0.47.1 | 0.47.0 | 0.46.3 | 0.46.2 | 0.46.1 | 0.46.0 | 0.45.0 | 0.44.0 | 0.43.7 | 0.43.6 | 0.43.5 | 0.43.4 | 0.43.3 | 0.43.2 | 0.43.1 | 0.43.0 | 0.42.1 | 0.42.0 | 0.41.1 | 0.41.0 | 0.40.2 | 0.40.1 | 0.40.0 | ++========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+ +| 0.62.0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.61.2 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.61.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.61.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.60.2 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.60.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.60.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.59.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.59.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.58.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.57.2 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.57.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.57.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.56.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.55.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.54.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.53.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.52.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.52.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.52.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.51.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.50.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.50.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.49.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.49.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.49.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.48.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.47.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.47.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.47.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.45.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.44.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.42.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.42.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.41.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.41.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.40.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.40.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.40.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ diff --git a/docs/source/logging.rst b/docs/source/logging.rst index 43a437a78..6a717a489 100644 --- a/docs/source/logging.rst +++ b/docs/source/logging.rst @@ -29,7 +29,7 @@ This is followed by the actual log message on the right: Colourised services ------------------- -Another advantage to using the Octue log handler is that each Octue service is coloured according to its position in the +Another advantage to using the Octue log handler is that each Twined service is coloured according to its position in the tree, making it much easier to read log messages from multiple levels of children. .. image:: images/coloured_logs.png diff --git a/docs/source/manifest.rst b/docs/source/manifest.rst index 0b299343e..95ca60368 100644 --- a/docs/source/manifest.rst +++ b/docs/source/manifest.rst @@ -8,11 +8,11 @@ Manifest :mod:`Manifest ` A set of related cloud and/or local :doc:`datasets `, metadata, and helper methods. Typically produced - by or needed for processing by an Octue service. + by or needed for processing by a Twined service. .. tip:: - Use a manifest to send :doc:`datasets ` to an Octue service as a question (for processing) - the service + Use a manifest to send :doc:`datasets ` to a Twined service as a question (for processing) - the service will send an output manifest back with its answer if the answer includes output datasets. @@ -38,7 +38,7 @@ Make a clear grouping of datasets needed for a particular analysis. Send datasets to a service -------------------------- -Get an Octue service to analyse data for you as part of a larger analysis. +Get a Twined service to analyse data for you as part of a larger analysis. .. code-block:: python @@ -56,7 +56,7 @@ See :doc:`here ` for more information. Receive datasets from a service ------------------------------- -Access output datasets from an Octue service from the cloud when you're ready. +Access output datasets from a Twined service from the cloud when you're ready. .. code-block:: python diff --git a/docs/source/running_services_locally.rst b/docs/source/running_services_locally.rst index ff42f4c29..88dc5f94f 100644 --- a/docs/source/running_services_locally.rst +++ b/docs/source/running_services_locally.rst @@ -5,10 +5,10 @@ Services can be operated locally (e.g. for testing or ad-hoc data processing). Y - Run your service once (i.e. run one analysis): - - Via the CLI + - Via the ``octue`` CLI - By using the ``octue`` library in a python script -- Start your service as a child, allowing it to answer any number of questions from any other Octue service: +- Start your service as a child, allowing it to answer any number of questions from any other Twined service: - Via the CLI @@ -19,22 +19,14 @@ Running a service once Via the CLI ----------- 1. Ensure you've created a valid :ref:`octue.yaml ` file for your service -2. If your service requires inputs, create an input directory with the following structure - - .. code-block:: shell - input_directory - |--- values.json (if input values are required) - |--- manifest.json (if an input manifest is required) - -3. Run: +2. Run: .. code-block:: shell - octue run --input-dir=my_input_directory + octue question ask local --input-values='{"some": "input"}' -Any output values will be printed to ``stdout`` and any output datasets will be referenced in an output manifest file -named ``output_manifest_.json``. +The output values and/or manifest will be printed to ``stdout`` but are also stored in the event store. Via a python script ------------------- diff --git a/docs/source/services.rst b/docs/source/services.rst index 2a71acd72..54fecd3d2 100644 --- a/docs/source/services.rst +++ b/docs/source/services.rst @@ -1,26 +1,25 @@ .. _services: -============== -Octue services -============== +===================== +Octue Twined services +===================== -There's a growing range of live :ref:`services ` in the Octue ecosystem that you can ask questions -to and get answers from. Currently, all of them are related to wind energy. Here's a quick glossary of terms before we -tell you more: +There's a growing range of live :ref:`services ` in the Octue ecosystem that you can query, mostly +related to wind energy and other renewables. Here's a quick glossary of terms before we tell you more: .. admonition:: Definitions - Octue service + Twined service See :ref:`here `. Child - An Octue service that can be asked a question. This name reflects the tree structure of services (specifically, + A Twined service that can be asked a question. This name reflects the tree structure of services (specifically, `a DAG `_) formed by the service asking the question (the parent), the child it asks the question to, any children that the child asks questions to as part of forming its answer, and so on. Parent - An Octue service that asks a question to another Octue service (a child). + A Twined service that asks a question to another Twined service (a child). Asking a question Sending data (input values and/or an input manifest) to a child for processing/analysis. @@ -28,7 +27,7 @@ tell you more: Receiving an answer Receiving data (output values and/or an output manifest) from a child you asked a question to. - Octue ecosystem + Twined ecosystem The set of services running the Octue SDK as their backend. These services guarantee: - Defined input/output JSON schemas and validation @@ -48,7 +47,7 @@ They look like ``namespace/name:tag`` where the tag is often a semantic version .. admonition:: Definitions Service revision - A specific instance of an Octue service that can be individually addressed. The revision could correspond to a + A specific instance of a Twined service that can be individually addressed. The revision could correspond to a version of the service, a dynamic development branch for it, or a deliberate duplication or variation of it. .. _sruid_definition: @@ -93,7 +92,7 @@ They look like ``namespace/name:tag`` where the tag is often a semantic version Service communication standard ============================== -Octue services communicate according to the service communication standard. The JSON schema defining this can be found +Twined services communicate according to the service communication standard. The JSON schema defining this can be found `here `_. Messages received by services are validated against it and invalid messages are rejected. The schema is in beta, so (rare) breaking changes are reflected in the minor version number. diff --git a/docs/source/testing_services.rst b/docs/source/testing_services.rst index 491067cc8..6c43f3b21 100644 --- a/docs/source/testing_services.rst +++ b/docs/source/testing_services.rst @@ -4,7 +4,7 @@ Testing services ================ We recommend writing automated tests for your service so anyone who wants to use it can have confidence in its quality -and reliability at a glance. `Here's an example test `_ +and reliability at a glance. `Here's an example test `_ for our example service. diff --git a/docs/source/troubleshooting_services.rst b/docs/source/troubleshooting_services.rst index 3e77d9aab..5f4c80cac 100644 --- a/docs/source/troubleshooting_services.rst +++ b/docs/source/troubleshooting_services.rst @@ -27,15 +27,15 @@ message. A user with credentials to access this path can use the ``octue`` CLI t .. code-block:: shell - octue get-diagnostics + octue question diagnostics More information on the command: .. code-block:: - >>> octue get-diagnostics -h + >>> octue question diagnostics -h - Usage: octue get-diagnostics [OPTIONS] CLOUD_PATH + Usage: octue question diagnostics [OPTIONS] CLOUD_PATH Download diagnostics for an analysis from the given directory in Google Cloud Storage. The cloud path should end in the analysis ID. diff --git a/docs/source/updating_services.rst b/docs/source/updating_services.rst index e4867e2ab..ead5eebdb 100644 --- a/docs/source/updating_services.rst +++ b/docs/source/updating_services.rst @@ -1,19 +1,19 @@ .. _updating_services: -Updating an Octue service +Updating a Twined service ========================= -This page describes how to update an existing, deployed Octue service - in other words, how to deploy a new Octue +This page describes how to update an existing, deployed Twined service - in other words, how to deploy a new Twined service revision. We assume that: - Your service's repository is on GitHub and you have push access to it -- Octue's `standard deployment GitHub Actions workflow `_ - is set up in the repository and being used to deploy the service to Google Cloud Run on merge of a pull request into - the ``main`` branch (see an example `here `_) +- The `standard Twined service deployment GitHub Actions workflow `_ + is set up in the repository and being used to build and push the service image to the artifact registry on merge of a + pull request into the ``main`` branch (see an example `here `_) - A release workflow is set up that will tag and release the new service revision on GitHub (see an example - `here `_) + `here `_) Instructions ------------- diff --git a/octue/__init__.py b/octue/__init__.py index 2e9ae9f5c..60da2ca73 100644 --- a/octue/__init__.py +++ b/octue/__init__.py @@ -4,16 +4,12 @@ from .log_handlers import apply_log_handler, should_use_octue_log_handler from .runner import Runner - logger = logging.getLogger(__name__) __all__ = ("Runner",) - - REPOSITORY_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) - if should_use_octue_log_handler(): apply_log_handler( logger_name=None, # Apply to the root logger. diff --git a/octue/cli.py b/octue/cli.py index 1add4f528..00d06b9af 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -1,7 +1,5 @@ import copy import functools -import importlib.metadata -import importlib.util import json import logging import os @@ -10,19 +8,24 @@ import click from google import auth -from octue.cloud import pub_sub, storage +from octue.cloud import storage +from octue.cloud.events.answer_question import answer_question +from octue.cloud.events.replayer import EventReplayer +from octue.cloud.events.question import make_question_event +from octue.cloud.events.validation import VALID_EVENT_KINDS +from octue.cloud.pub_sub.bigquery import get_events, DEFAULT_EVENT_STORE_TABLE_ID from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts from octue.cloud.storage import GoogleCloudStorageClient -from octue.configuration import load_service_and_app_configuration -from octue.definitions import MANIFEST_FILENAME, VALUES_FILENAME +from octue.configuration import ServiceConfiguration, load_service_and_app_configuration +from octue.definitions import LOCAL_SDK_VERSION, MANIFEST_FILENAME, VALUES_FILENAME from octue.exceptions import ServiceAlreadyExists from octue.log_handlers import apply_log_handler, get_remote_handler -from octue.resources import Manifest, service_backends +from octue.resources import Child, Manifest, service_backends from octue.runner import Runner +from octue.utils.decoders import OctueJSONDecoder from octue.utils.encoders import OctueJSONEncoder - logger = logging.getLogger(__name__) global_cli_context = {} @@ -51,7 +54,7 @@ show_default=True, help="Forces a reset of analysis cache and outputs [For future use, currently not implemented]", ) -@click.version_option(version=importlib.metadata.version("octue")) +@click.version_option(version=LOCAL_SDK_VERSION) def octue_cli(id, logger_uri, log_level, force_reset): """The CLI for the Octue SDK. Use it to start an Octue data service or digital twin locally or run an analysis on one locally. @@ -70,167 +73,163 @@ def octue_cli(id, logger_uri, log_level, force_reset): global_cli_context["log_handler"] = get_remote_handler(logger_uri=global_cli_context["logger_uri"]) -@octue_cli.command() +@octue_cli.group() +def question(): + """Ask a new question to an Octue Twined data service or interact with a previous question.""" + + +@question.group() +def ask(): + """Ask a new question to an Octue Twined data service.""" + + +@ask.command() +@click.argument("sruid", type=str) @click.option( - "-c", - "--service-config", - type=click.Path(dir_okay=False), + "-i", + "--input-values", + type=str, default=None, - help="The path to an `octue.yaml` file defining the service to run. If not provided, the " - "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " - "is used.", + help="Any input values for the question as a JSON-encoded string.", ) @click.option( - "--input-dir", - type=click.Path(file_okay=False, exists=True), - default=".", - show_default=True, - help="The path to a directory containing the input values (in a file called 'values.json') and/or input manifest " - "(in a file called 'manifest.json').", + "-m", + "--input-manifest", + type=str, + default=None, + help="An optional input manifest for the question serialised as a JSON-encoded string.", ) @click.option( - "-o", - "--output-file", - type=click.Path(dir_okay=False), + "-p", + "--project-name", + type=str, default=None, - show_default=True, - help="The path to a JSON file to store the output values in, if required.", + help="The name of the Google Cloud project the service is deployed in. If not provided, the project name is " + "detected from the local Google application credentials if present.", ) @click.option( - "--output-manifest-file", - type=click.Path(dir_okay=False), - default=None, - help="The path to a JSON file to store the output manifest in. The default is 'output_manifest_.json'.", + "--asynchronous", + is_flag=True, + help="If provided, ask the question and detach. The result and other events can be retrieved from the event store " + "later.", ) @click.option( - "--monitor-messages-file", + "-c", + "--service-config", type=click.Path(dir_okay=False), default=None, - show_default=True, - help="The path to a JSON file in which to store any monitor messages received. Monitor messages will be ignored " - "if this option isn't provided.", + help="An optional path to an `octue.yaml` file defining service registries to use. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", ) -def run(service_config, input_dir, output_file, output_manifest_file, monitor_messages_file): - """Run an analysis on the given input data using an Octue service or digital twin locally. The output values are - printed to `stdout`. If an output manifest is produced, it will be saved locally (see the `--output-manifest-file` - option). - """ - service_configuration, app_configuration = load_service_and_app_configuration(service_config) - - input_values_path = os.path.join(input_dir, VALUES_FILENAME) - input_manifest_path = os.path.join(input_dir, MANIFEST_FILENAME) +def remote(sruid, input_values, input_manifest, project_name, asynchronous, service_config): + """Ask a question to a remote Octue Twined service. - input_values = None - input_manifest = None + SRUID should be a valid service revision unique identifier for an existing Octue Twined service e.g. - if os.path.exists(input_values_path): - input_values = input_values_path + octue question ask remote your-org/example-service:1.2.0 + """ + service_configuration = ServiceConfiguration.from_file(service_config, allow_not_found=True) - if os.path.exists(input_manifest_path): - input_manifest = input_manifest_path + if service_configuration: + service_registries = service_configuration.service_registries + else: + service_registries = None - runner = Runner.from_configuration(service_configuration=service_configuration, app_configuration=app_configuration) + if input_values: + input_values = json.loads(input_values, cls=OctueJSONDecoder) - if monitor_messages_file: - if not os.path.exists(os.path.dirname(monitor_messages_file)): - os.makedirs(os.path.dirname(monitor_messages_file)) + if input_manifest: + input_manifest = Manifest.deserialise(input_manifest, from_string=True) - monitor_message_handler = lambda message: _add_monitor_message_to_file(monitor_messages_file, message) + if not project_name: + _, project_name = auth.default() - else: - monitor_message_handler = None + child = Child( + id=sruid, + backend={"name": "GCPPubSubBackend", "project_name": project_name}, + service_registries=service_registries, + ) - analysis = runner.run( - analysis_id=global_cli_context["analysis_id"], + answer, question_uuid = child.ask( input_values=input_values, input_manifest=input_manifest, - analysis_log_level=global_cli_context["log_level"], - analysis_log_handler=global_cli_context["log_handler"], - handle_monitor_message=monitor_message_handler, + asynchronous=asynchronous, ) - click.echo(json.dumps(analysis.output_values, cls=OctueJSONEncoder)) - - if analysis.output_values and output_file: - if not os.path.exists(os.path.dirname(output_file)): - os.makedirs(os.path.dirname(output_file)) - - with open(output_file, "w") as f: - json.dump(analysis.output_values, f, cls=OctueJSONEncoder, indent=4) + if asynchronous: + click.echo(question_uuid) + return - if analysis.output_manifest: - if not os.path.exists(os.path.dirname(output_manifest_file)): - os.makedirs(os.path.dirname(output_manifest_file)) + output_manifest = answer.get("output_manifest") - with open(output_manifest_file or f"output_manifest_{analysis.id}.json", "w") as f: - json.dump(analysis.output_manifest.to_primitive(), f, cls=OctueJSONEncoder, indent=4) + if output_manifest: + answer["output_manifest"] = output_manifest.to_primitive() - return 0 + click.echo(json.dumps(answer, cls=OctueJSONEncoder)) -@octue_cli.command() +@ask.command() @click.option( - "-c", - "--service-config", - type=click.Path(dir_okay=False), - default="octue.yaml", - help="The path to an `octue.yaml` file defining the service to start.", + "-i", + "--input-values", + type=str, + default=None, + help="Any input values for the question, serialised as a JSON-encoded string.", ) @click.option( - "--revision-tag", + "-m", + "--input-manifest", type=str, default=None, - help="A tag to use for this revision of the service (e.g. 1.3.7). This overrides the `OCTUE_SERVICE_REVISION_TAG` " - "environment variable if it's present. If this option isn't given and the environment variable isn't present, a " - "random 'cool name' tag is generated e.g 'curious-capybara'.", + help="An optional input manifest for the question, serialised as a JSON-encoded string.", ) @click.option( - "--timeout", - type=click.INT, + "-a", + "--attributes", + type=str, default=None, - show_default=True, - help="A timeout in seconds after which to stop the service. The default is no timeout.", + help="An optional full set of event attributes for the question, serialised as a JSON-encoded string. If not " + "provided, the question will be an originator question.", ) @click.option( - "--no-rm", - is_flag=True, - default=False, - show_default=True, - help="Don't delete the Google Pub/Sub topic and subscription for the service on exit.", + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="The path to an `octue.yaml` file defining the service to run. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", ) -def start(service_config, revision_tag, timeout, no_rm): - """Start an Octue service or digital twin locally as a child so it can be asked questions by other Octue services. - The service's pub/sub topic and subscription are deleted on exit. - """ - service_revision_tag_override = revision_tag - service_configuration, app_configuration = load_service_and_app_configuration(service_config) - service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) +def local(input_values, input_manifest, attributes, service_config): + """Ask a question to a local Octue Twined service. - if service_revision_tag_override and service_revision_tag: - logger.warning( - "The `OCTUE_SERVICE_REVISION_TAG` environment variable %r has been overridden by the `--revision-tag` CLI " - "option %r.", - service_revision_tag, - service_revision_tag_override, - ) + This command is similar to running `octue start` and asking the resulting local service revision a question + via Pub/Sub. Instead of starting a local Pub/Sub service revision, however, no Pub/Sub subscription or subscriber is + created; the question is instead passed directly to local the service revision without Pub/Sub being involved. + Everything after this runs the same, though, with the service revision emitting any events via Pub/Sub as usual. + """ + if input_values: + input_values = json.loads(input_values, cls=OctueJSONDecoder) - service_sruid = create_sruid( - namespace=service_namespace, - name=service_name, - revision_tag=service_revision_tag_override or service_revision_tag, - ) + if input_manifest: + input_manifest = json.loads(input_manifest, cls=OctueJSONDecoder) - runner = Runner.from_configuration( - service_configuration=service_configuration, - app_configuration=app_configuration, - service_id=service_sruid, - ) + service_configuration, app_configuration = load_service_and_app_configuration(service_config) - run_function = functools.partial( - runner.run, - analysis_log_level=global_cli_context["log_level"], - analysis_log_handler=global_cli_context["log_handler"], - ) + if attributes: + attributes = json.loads(attributes, cls=OctueJSONDecoder) + question = make_question_event(input_values=input_values, input_manifest=input_manifest, attributes=attributes) + else: + namespace, name, revision_tag = get_sruid_parts(service_configuration) + recipient = create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) + + question = make_question_event( + input_values=input_values, + input_manifest=input_manifest, + sender=create_sruid(), + recipient=recipient, + ) backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") @@ -242,32 +241,248 @@ def start(service_config, revision_tag, timeout, no_rm): _, project_name = auth.default() backend = service_backends.get_backend()(project_name=project_name) - service = Service(service_id=service_sruid, backend=backend, run_function=run_function) + answer = answer_question( + question=question, + project_name=backend.project_name, + service_configuration=service_configuration, + app_configuration=app_configuration, + ) - try: - service.serve(timeout=timeout, delete_topic_and_subscription_on_exit=not no_rm) + click.echo(json.dumps(answer, cls=OctueJSONEncoder)) - except ServiceAlreadyExists: - # Generate and use a new revision tag if the service already exists. - service_sruid = create_sruid(namespace=service_namespace, name=service_name) - while True: - user_confirmation = input( - "Service already exists. Create new service with service revision unique identifier (SRUID) " - f"{service_sruid!r}? [Y/n]\n" - ) +@question.group() +def events(): + """Get and replay events from past and current questions.""" - if user_confirmation.upper() == "N": - return - if user_confirmation.upper() in {"Y", ""}: - break +@events.command() +@click.option( + "--question-uuid", + type=str, + default=None, + help="The UUID of the question to get events for.", +) +@click.option( + "--parent-question-uuid", + type=str, + default=None, + help="The UUID of a parent question to get the sub-question events for.", +) +@click.option( + "--originator-question-uuid", + type=str, + default=None, + help="The UUID of an originator question get the full tree of events for.", +) +@click.option( + "-k", + "--kinds", + type=str, + default=None, + help="The kinds of event to get as a comma-separated list e.g. 'question,result'. If not provided, all event kinds " + f"are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", +) +@click.option( + "-e", + "--exclude-kinds", + type=str, + default=None, + help="The kinds of event to exclude as a comma-separated list e.g. 'question,result'. If not provided, all event " + f"kinds are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", +) +@click.option( + "--include-backend-metadata", + is_flag=True, + help="Include the service backend metadata.", +) +@click.option( + "-l", + "--limit", + type=int, + default=1000, + show_default=True, + help="Limit the number of events returned.", +) +@click.option( + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="The path to an `octue.yaml` file defining the service to run. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", +) +def get( + question_uuid, + parent_question_uuid, + originator_question_uuid, + kinds, + exclude_kinds, + include_backend_metadata, + limit, + service_config, +): + """Get the events emitted during a question as JSON. One of the following must be set: - service = Service(service_id=service_sruid, backend=backend, run_function=run_function) - service.serve(timeout=timeout, delete_topic_and_subscription_on_exit=not no_rm) + --question-uuid\n + --parent-question-uuid\n + --originator-question-uuid\n + """ + if kinds: + kinds = kinds.split(",") + if exclude_kinds: + exclude_kinds = exclude_kinds.split(",") -@octue_cli.command() + service_configuration = ServiceConfiguration.from_file(path=service_config, allow_not_found=True) + + if service_configuration: + event_store_table_id = service_configuration.event_store_table_id + else: + event_store_table_id = DEFAULT_EVENT_STORE_TABLE_ID + + events = get_events( + table_id=event_store_table_id, + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + kinds=kinds, + exclude_kinds=exclude_kinds, + include_backend_metadata=include_backend_metadata, + limit=limit, + ) + + click.echo(json.dumps(events, cls=OctueJSONEncoder)) + + +@events.command() +@click.option( + "--question-uuid", + type=str, + default=None, + help="The UUID of the question to get events for.", +) +@click.option( + "--parent-question-uuid", + type=str, + help="The UUID of a parent question to get the sub-question events for.", +) +@click.option( + "--originator-question-uuid", + type=str, + help="The UUID of an originator question get the full tree of events for.", +) +@click.option( + "-k", + "--kinds", + type=str, + default=None, + help="The kinds of event to get as a comma-separated list e.g. 'question,result'. If not provided, all event kinds " + f"are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", +) +@click.option( + "-e", + "--exclude-kinds", + type=str, + default=None, + help="The kinds of event to exclude as a comma-separated list e.g. 'question,result'. If not provided, all event " + f"kinds are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", +) +@click.option( + "-l", + "--limit", + type=int, + default=1000, + show_default=True, + help="Limit the number of events returned.", +) +@click.option( + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="The path to an `octue.yaml` file defining the service to run. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", +) +@click.option( + "--include-service-metadata", + is_flag=True, + help="Include the SRUIDs and question UUIDs of the service revisions involved in the question at the start of each " + "log message. This is useful when a child asks its own sub-questions.", +) +@click.option( + "--exclude-logs-containing", + type=str, + default=None, + help="Skip handling log messages containing this string.", +) +@click.option( + "--validate-events", + is_flag=True, + help="Validate events before attempting to handle them (this is off by default to speed up event handling)", +) +def replay( + question_uuid, + parent_question_uuid, + originator_question_uuid, + kinds, + exclude_kinds, + limit, + service_config, + include_service_metadata, + exclude_logs_containing, + validate_events, +): + """Replay a question's events, returning the result as JSON at the end if there is one. One of the following must be + set: + + --question-uuid\n + --parent-question-uuid\n + --originator-question-uuid\n + """ + if kinds: + kinds = kinds.split(",") + + if exclude_kinds: + exclude_kinds = exclude_kinds.split(",") + + service_configuration = ServiceConfiguration.from_file(path=service_config, allow_not_found=True) + + if service_configuration: + event_store_table_id = service_configuration.event_store_table_id + else: + event_store_table_id = DEFAULT_EVENT_STORE_TABLE_ID + + events = get_events( + table_id=event_store_table_id, + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + kinds=kinds, + exclude_kinds=exclude_kinds, + limit=limit, + ) + + if not events: + return + + replayer = EventReplayer( + include_service_metadata_in_logs=include_service_metadata, + exclude_logs_containing=exclude_logs_containing, + validate_events=validate_events, + ) + + result = replayer.handle_events(events) + + if not result: + return + + click.echo(json.dumps(result, cls=OctueJSONEncoder)) + + +@question.command() @click.argument( "cloud_path", type=str, @@ -285,7 +500,7 @@ def start(service_config, revision_tag, timeout, no_rm): help="If provided, download any datasets from the diagnostics and update their paths in the configuration and " "input manifests to the new local paths.", ) -def get_diagnostics(cloud_path, local_path, download_datasets): +def diagnostics(cloud_path, local_path, download_datasets): """Download diagnostics for a question from the given directory in Google Cloud Storage. The cloud path should end in the question ID. @@ -337,74 +552,158 @@ def get_diagnostics(cloud_path, local_path, download_datasets): logger.info("Downloaded diagnostics from %r to %r.", cloud_path, local_path) -@octue_cli.group() -def deploy(): - """A collection of commands to aid deploying a python app to the cloud as an Octue service or digital twin.""" +# @question.command() +# @click.argument("question_uuid", type=str) +# @click.option( +# "-p", +# "--project-name", +# type=str, +# default=None, +# help="If asking a remote question, the name of the Google Cloud project the service is deployed in. If not " +# "provided, the project name is detected from the local Google application credentials if present.", +# ) +# @click.option( +# "-c", +# "--service-config", +# type=click.Path(dir_okay=False), +# default=None, +# help="An optional path to an `octue.yaml` file defining service registries to use. If not provided, the " +# "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " +# "is used.", +# ) +# def cancel(question_uuid, project_name, service_config): +# """Cancel a question running on an Octue Twined service. +# +# QUESTION_UUID: The question UUID of a running question +# """ +# service_configuration = ServiceConfiguration.from_file(path=service_config) +# +# if not project_name: +# _, project_name = auth.default() +# +# child = Child(id=None, backend={"name": "GCPPubSubBackend", "project_name": project_name}) +# child.cancel(question_uuid=question_uuid, event_store_table_id=service_configuration.event_store_table_id) + + +@octue_cli.command(deprecated=True) +@click.argument( + "cloud_path", + type=str, +) +@click.option( + "--local-path", + type=click.Path(file_okay=False), + default=".", + help="The path to a directory to store the directory of diagnostics data in. Defaults to the current working " + "directory.", +) +@click.option( + "--download-datasets", + is_flag=True, + help="If provided, download any datasets from the diagnostics and update their paths in the configuration and " + "input manifests to the new local paths.", +) +def get_diagnostics(cloud_path, local_path, download_datasets): + diagnostics(cloud_path, local_path, download_datasets) -@deploy.command() -@click.argument("project_name") -@click.argument("service_namespace") -@click.argument("service_name") -@click.argument("push_endpoint") +@octue_cli.command() @click.option( - "--expiration-time", - is_flag=False, - default=None, - show_default=True, - help="The number of seconds of inactivity after which the subscription should expire. If not provided, no " - "expiration time is applied to the subscription.", + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default="octue.yaml", + help="The path to an `octue.yaml` file defining the service to start.", ) @click.option( "--revision-tag", - is_flag=False, + type=str, + default=None, + help="A tag to use for this revision of the service (e.g. 1.3.7). This overrides the `OCTUE_SERVICE_REVISION_TAG` " + "environment variable if it's present. If this option isn't given and the environment variable isn't present, a " + "random 'cool name' tag is generated e.g 'curious-capybara'.", +) +@click.option( + "--timeout", + type=click.INT, default=None, show_default=True, - help="The service revision tag (e.g. 1.0.7). If this option isn't given, a random 'cool name' tag is generated e.g" - ". 'curious-capybara'.", + help="A timeout in seconds after which to stop the service. The default is no timeout.", ) @click.option( - "--no-allow-existing", + "--no-rm", is_flag=True, - help="If provided, raise an error if the push subscription already exists.", -) -def create_push_subscription( - project_name, - service_namespace, - service_name, - push_endpoint, - expiration_time, - revision_tag, - no_allow_existing, -): - """Create a Google Pub/Sub push subscription for an Octue service for it to receive questions from parents. The - subscription name is printed on completion. + default=False, + show_default=True, + help="Don't delete the Google Pub/Sub topic and subscription for the service on exit.", +) +def start(service_config, revision_tag, timeout, no_rm): + """Start an Octue service or digital twin locally as a child so it can be asked questions by other Octue services. + The service's pub/sub topic and subscription are deleted on exit. + """ + service_revision_tag_override = revision_tag + service_configuration, app_configuration = load_service_and_app_configuration(service_config) + service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) - PROJECT_NAME is the name of the Google Cloud project in which the subscription will be created + if service_revision_tag_override and service_revision_tag: + logger.warning( + "The `OCTUE_SERVICE_REVISION_TAG` environment variable %r has been overridden by the `--revision-tag` CLI " + "option %r.", + service_revision_tag, + service_revision_tag_override, + ) - SERVICE_NAMESPACE is the namespace the service belongs to in kebab case + service_sruid = create_sruid( + namespace=service_namespace, + name=service_name, + revision_tag=service_revision_tag_override or service_revision_tag, + ) - SERVICE_NAME is the name of the service in kebab case, unique within its namespace + runner = Runner.from_configuration( + service_configuration=service_configuration, + app_configuration=app_configuration, + service_id=service_sruid, + ) - PUSH_ENDPOINT is the HTTP/HTTPS endpoint of the service to push to. It should be fully formed and include the - 'https://' prefix - """ - sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=revision_tag) - - subscription = pub_sub.create_push_subscription( - project_name, - sruid, - push_endpoint, - expiration_time=expiration_time, - subscription_filter=f'attributes.recipient = "{sruid}" AND attributes.sender_type = "PARENT"', - allow_existing=not no_allow_existing, + run_function = functools.partial( + runner.run, + analysis_log_level=global_cli_context["log_level"], + analysis_log_handler=global_cli_context["log_handler"], ) - if subscription.creation_triggered_locally: - click.echo(f"Subscription for {sruid!r} created.") - return + backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") - click.echo(f"Subscription for {sruid!r} already exists.") + if backend_configuration_values: + backend_configuration_values = copy.deepcopy(backend_configuration_values) + backend = service_backends.get_backend(backend_configuration_values.pop("name"))(**backend_configuration_values) + else: + # If no backend details are provided, use Google Pub/Sub with the default project. + _, project_name = auth.default() + backend = service_backends.get_backend()(project_name=project_name) + + service = Service(service_id=service_sruid, backend=backend, run_function=run_function) + + try: + service.serve(timeout=timeout, delete_topic_and_subscription_on_exit=not no_rm) + + except ServiceAlreadyExists: + # Generate and use a new revision tag if the service already exists. + service_sruid = create_sruid(namespace=service_namespace, name=service_name) + + while True: + user_confirmation = input( + "Service already exists. Create new service with service revision unique identifier (SRUID) " + f"{service_sruid!r}? [Y/n]\n" + ) + + if user_confirmation.upper() == "N": + return + + if user_confirmation.upper() in {"Y", ""}: + break + + service = Service(service_id=service_sruid, backend=backend, run_function=run_function) + service.serve(timeout=timeout, delete_topic_and_subscription_on_exit=not no_rm) def _add_monitor_message_to_file(path, monitor_message): diff --git a/octue/cloud/__init__.py b/octue/cloud/__init__.py index 1f32a2282..13c386b10 100644 --- a/octue/cloud/__init__.py +++ b/octue/cloud/__init__.py @@ -1,7 +1,6 @@ import octue.exceptions -import twined.exceptions from octue.utils.exceptions import create_exceptions_mapping - +import twined.exceptions EXCEPTIONS_MAPPING = create_exceptions_mapping( globals()["__builtins__"], diff --git a/octue/cloud/deployment/google/cloud_run/Dockerfile-python310 b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 similarity index 76% rename from octue/cloud/deployment/google/cloud_run/Dockerfile-python310 rename to octue/cloud/deployment/dockerfiles/Dockerfile-python310 index a3ca69a0b..693fc1b4d 100644 --- a/octue/cloud/deployment/google/cloud_run/Dockerfile-python310 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 @@ -34,16 +34,6 @@ RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ elif [ -f "setup.py" ]; then pip install --upgrade pip && pip install -e .; \ elif [ -f "requirements.txt" ]; then pip install --upgrade pip && pip install -r requirements.txt; fi -EXPOSE $PORT - ENV USE_OCTUE_LOG_HANDLER=1 -ENV COMPUTE_PROVIDER=GOOGLE_CLOUD_RUN - -ARG GUNICORN_WORKERS=1 -ENV GUNICORN_WORKERS=$GUNICORN_WORKERS - -ARG GUNICORN_THREADS=8 -ENV GUNICORN_THREADS=$GUNICORN_THREADS - -# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. -CMD exec gunicorn --bind :$PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout 0 octue.cloud.deployment.google.cloud_run.flask_app:app +ENV COMPUTE_PROVIDER=GOOGLE_KUEUE +CMD ["octue", "question", "ask", "local"] diff --git a/octue/cloud/deployment/google/cloud_run/Dockerfile-python311 b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 similarity index 76% rename from octue/cloud/deployment/google/cloud_run/Dockerfile-python311 rename to octue/cloud/deployment/dockerfiles/Dockerfile-python311 index 03d99cd73..37e36d875 100644 --- a/octue/cloud/deployment/google/cloud_run/Dockerfile-python311 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 @@ -34,16 +34,6 @@ RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ elif [ -f "setup.py" ]; then pip install --upgrade pip && pip install -e .; \ elif [ -f "requirements.txt" ]; then pip install --upgrade pip && pip install -r requirements.txt; fi -EXPOSE $PORT - ENV USE_OCTUE_LOG_HANDLER=1 -ENV COMPUTE_PROVIDER=GOOGLE_CLOUD_RUN - -ARG GUNICORN_WORKERS=1 -ENV GUNICORN_WORKERS=$GUNICORN_WORKERS - -ARG GUNICORN_THREADS=8 -ENV GUNICORN_THREADS=$GUNICORN_THREADS - -# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. -CMD exec gunicorn --bind :$PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout 0 octue.cloud.deployment.google.cloud_run.flask_app:app +ENV COMPUTE_PROVIDER=GOOGLE_KUEUE +CMD ["octue", "question", "ask", "local"] diff --git a/octue/cloud/deployment/google/__init__.py b/octue/cloud/deployment/google/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/octue/cloud/deployment/google/answer_pub_sub_question.py b/octue/cloud/deployment/google/answer_pub_sub_question.py deleted file mode 100644 index 934fd87e9..000000000 --- a/octue/cloud/deployment/google/answer_pub_sub_question.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging - -from octue.cloud.pub_sub.service import Service -from octue.cloud.service_id import create_sruid, get_sruid_parts -from octue.configuration import load_service_and_app_configuration -from octue.resources.service_backends import GCPPubSubBackend -from octue.runner import Runner -from octue.utils.objects import get_nested_attribute - - -logger = logging.getLogger(__name__) - - -def answer_question(question, project_name): - """Answer a question sent to an app deployed in Google Cloud. - - :param dict|tuple question: - :param str project_name: - :return None: - """ - service_configuration, app_configuration = load_service_and_app_configuration() - service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) - - service_sruid = create_sruid( - namespace=service_namespace, - name=service_name, - revision_tag=service_revision_tag, - ) - - service = Service(service_id=service_sruid, backend=GCPPubSubBackend(project_name=project_name)) - question_uuid = get_nested_attribute(question, "attributes.question_uuid") - - try: - runner = Runner.from_configuration( - service_configuration=service_configuration, - app_configuration=app_configuration, - project_name=project_name, - service_id=service_sruid, - ) - - service.run_function = runner.run - service.answer(question) - logger.info("Analysis successfully run and response sent for question %r.", question_uuid) - - except BaseException as error: # noqa - service.send_exception( - question_uuid=question_uuid, - parent_question_uuid=get_nested_attribute(question, "attributes.parent_question_uuid"), - originator_question_uuid=get_nested_attribute(question, "attributes.originator_question_uuid"), - parent=get_nested_attribute(question, "attributes.parent"), - originator=get_nested_attribute(question, "attributes.originator"), - retry_count=get_nested_attribute(question, "attributes.retry_count"), - ) - - logger.exception(error) diff --git a/octue/cloud/deployment/google/cloud_run/Dockerfile-python39 b/octue/cloud/deployment/google/cloud_run/Dockerfile-python39 deleted file mode 100644 index a76513322..000000000 --- a/octue/cloud/deployment/google/cloud_run/Dockerfile-python39 +++ /dev/null @@ -1,49 +0,0 @@ -FROM windpioneers/gdal-python:little-gecko-gdal-2.4.1-python-3.9-slim - -# Ensure print statements and log messages appear promptly in Cloud Logging. -ENV PYTHONUNBUFFERED=True - -ENV PROJECT_ROOT=/workspace -WORKDIR $PROJECT_ROOT - -RUN apt-get update -y && apt-get install -y --fix-missing build-essential && rm -rf /var/lib/apt/lists/* - -# Install poetry. -ENV POETRY_HOME=/root/.poetry -ENV PATH="$POETRY_HOME/bin:$PATH" -RUN curl -sSL https://install.python-poetry.org | python3 - && poetry config virtualenvs.create false; - -# Copy in the dependencies file(s) for caching. One or more of `requirements.txt`, `setup.py`, and `pyproject.toml and -# `poetry.lock` must be present. -COPY pyproject.tom[l] poetry.loc[k] setup.p[y] requirements.tx[t] ./ - -# If `pyproject.toml` is present, install the dependencies only to utilise layer caching for quick rebuilds. -RUN if [ -f "pyproject.toml" ]; then poetry install \ - --no-ansi \ - --no-interaction \ - --no-cache \ - --no-root \ - --only main; \ - fi - -# Copy local code to the application root directory. -COPY . . - -# Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. -RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ - elif [ -f "setup.py" ]; then pip install --upgrade pip && pip install -e .; \ - elif [ -f "requirements.txt" ]; then pip install --upgrade pip && pip install -r requirements.txt; fi - -EXPOSE $PORT - -ENV USE_OCTUE_LOG_HANDLER=1 -ENV COMPUTE_PROVIDER=GOOGLE_CLOUD_RUN - -ARG GUNICORN_WORKERS=1 -ENV GUNICORN_WORKERS=$GUNICORN_WORKERS - -ARG GUNICORN_THREADS=8 -ENV GUNICORN_THREADS=$GUNICORN_THREADS - -# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. -CMD exec gunicorn --bind :$PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout 0 octue.cloud.deployment.google.cloud_run.flask_app:app diff --git a/octue/cloud/deployment/google/cloud_run/__init__.py b/octue/cloud/deployment/google/cloud_run/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/octue/cloud/deployment/google/cloud_run/flask_app.py b/octue/cloud/deployment/google/cloud_run/flask_app.py deleted file mode 100644 index ded293d9b..000000000 --- a/octue/cloud/deployment/google/cloud_run/flask_app.py +++ /dev/null @@ -1,111 +0,0 @@ -import logging - -import google.api_core.exceptions -from flask import Flask, request - -from octue.cloud.deployment.google.answer_pub_sub_question import answer_question -from octue.cloud.pub_sub.bigquery import get_events -from octue.configuration import ServiceConfiguration - - -logger = logging.getLogger(__name__) -app = Flask(__name__) - - -QUESTION_ACKNOWLEDGMENT_RESPONSE = ("", 204) - - -@app.route("/", methods=["POST"]) -def index(): - """Receive questions from Google Cloud Run in the form of Google Pub/Sub messages. - - :return (str, int): - """ - envelope = request.get_json() - - if not envelope: - return _log_bad_request_and_return_400_response("No Pub/Sub message received.") - - if not isinstance(envelope, dict) or "message" not in envelope: - return _log_bad_request_and_return_400_response(f"Invalid Pub/Sub message format - received {envelope!r}.") - - question = envelope["message"] - - if "data" not in question or "attributes" not in question or "question_uuid" not in question["attributes"]: - return _log_bad_request_and_return_400_response(f"Invalid Pub/Sub message format - received {envelope!r}.") - - should_drop_question = _check_if_should_drop_question( - question_uuid=question["attributes"].get("question_uuid"), - retry_count=question["attributes"].get("retry_count"), - ) - - if should_drop_question: - return QUESTION_ACKNOWLEDGMENT_RESPONSE - - project_name = envelope["subscription"].split("/")[1] - answer_question(question=question, project_name=project_name) - return QUESTION_ACKNOWLEDGMENT_RESPONSE - - -def _log_bad_request_and_return_400_response(message): - """Log an error return a bad request (400) response. - - :param str message: - :return (str, int): - """ - logger.error(message) - return (f"Bad Request: {message}", 400) - - -def _check_if_should_drop_question(question_uuid, retry_count): - """Check if the question has been delivered before and should be dropped. If it's a new question or if the question - is an explicit retry (i.e. it's been received before but the retry count is unique), return `False`. - - To determine if a question is new, the event store is checked for a delivery acknowledgement for the question UUID. - If no event store is specified or the specified event store can't be found, return `False`. - - :param str question_uuid: the UUID of the question to check - :param str retry_count: the retry count of the question to check (an integer in string form) - :return bool: whether the question should be dropped - """ - service_configuration = ServiceConfiguration.from_file() - - if not service_configuration.event_store_table_id: - logger.warning( - "Cannot check if question has been redelivered as the 'event_store_table_id' key hasn't been set in the " - "service configuration (`octue.yaml` file)." - ) - return False - - try: - previous_question_attempts = get_events( - table_id=service_configuration.event_store_table_id, - question_uuid=question_uuid, - exclude_kinds=["question"], - ) - - except google.api_core.exceptions.NotFound: - logger.warning( - "Cannot check if question has been redelivered as no event store table was found with the ID %r; check " - "that the 'event_store_table_id' key in the service configuration (`octue.yaml` file) is correct.", - service_configuration.event_store_table_id, - ) - return False - - # If there are no events for this question UUID, assume this is the first attempt for the question. - if not previous_question_attempts: - logger.info("Question %r (retry count %s) is a new question.", question_uuid, retry_count) - return False - - # Acknowledge redelivered questions to stop further redundant redelivery and processing. - for event in previous_question_attempts: - if event["attributes"]["retry_count"] == retry_count: - logger.warning( - "Question %r (retry count %s) has already been received by the service. It will now be acknowledged " - "and dropped to prevent further redundant redelivery.", - question_uuid, - retry_count, - ) - return True - - return False diff --git a/octue/cloud/emulators/_pub_sub.py b/octue/cloud/emulators/_pub_sub.py index 219db40fd..eb148d9a2 100644 --- a/octue/cloud/emulators/_pub_sub.py +++ b/octue/cloud/emulators/_pub_sub.py @@ -1,17 +1,17 @@ -import importlib.metadata +from collections import defaultdict import json import logging -from collections import defaultdict import google.api_core +from octue.cloud.events.attributes import QuestionAttributes from octue.cloud.pub_sub import Subscription, Topic from octue.cloud.pub_sub.service import PARENT_SENDER_TYPE, Service +from octue.definitions import LOCAL_SDK_VERSION from octue.resources import Manifest from octue.utils.dictionaries import make_minimal_dictionary from octue.utils.encoders import OctueJSONEncoder - logger = logging.getLogger(__name__) TOPICS = set() @@ -338,8 +338,11 @@ def ask( push_endpoint=None, asynchronous=False, retry_count=0, + cpus=None, + memory=None, + ephemeral_storage=None, timeout=86400, - parent_sdk_version=importlib.metadata.version("octue"), + parent_sdk_version=LOCAL_SDK_VERSION, ): """Put the question into the messages register, register the existence of the corresponding response topic, add the response to the register, and return a MockFuture containing the answer subscription path. @@ -358,6 +361,9 @@ def ask( :param str|None push_endpoint: :param bool asynchronous: :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param int|None cpus: + :param str|None memory: + :param str|None ephemeral_storage: :param float|None timeout: :param str parent_sdk_version: :return MockFuture, str: @@ -377,6 +383,9 @@ def ask( push_endpoint=push_endpoint, asynchronous=asynchronous, retry_count=retry_count, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, timeout=timeout, ) @@ -397,27 +406,27 @@ def ask( # If the originator isn't provided, assume that this service revision is the originator. originator = originator or self.id + attributes = QuestionAttributes( + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + forward_logs=subscribe_to_logs, + save_diagnostics=save_diagnostics, + parent=self.id, + originator=originator, + sender=self.id, + sender_type=PARENT_SENDER_TYPE, + sender_sdk_version=parent_sdk_version, + recipient=service_id, + retry_count=retry_count, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, + ) + try: self.children[service_id].answer( - MockMessage.from_primitive( - data=question, - attributes={ - "datetime": "2024-04-11T10:46:48.236064", - "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", - "question_uuid": question_uuid, - "parent_question_uuid": parent_question_uuid, - "originator_question_uuid": originator_question_uuid, - "forward_logs": subscribe_to_logs, - "save_diagnostics": save_diagnostics, - "parent": self.id, - "originator": originator, - "sender": self.id, - "sender_type": PARENT_SENDER_TYPE, - "sender_sdk_version": parent_sdk_version, - "recipient": service_id, - "retry_count": retry_count, - }, - ) + MockMessage.from_primitive(data=question, attributes=attributes.to_serialised_attributes()) ) except Exception as e: # noqa logger.exception(e) diff --git a/octue/cloud/events/__init__.py b/octue/cloud/events/__init__.py index 66898c235..2762bb477 100644 --- a/octue/cloud/events/__init__.py +++ b/octue/cloud/events/__init__.py @@ -1 +1,3 @@ -OCTUE_SERVICES_PREFIX = "octue.services" +import os + +OCTUE_SERVICES_TOPIC_NAME = os.environ.get("OCTUE_SERVICES_TOPIC_NAME", "main.octue.services") diff --git a/octue/cloud/events/answer_question.py b/octue/cloud/events/answer_question.py new file mode 100644 index 000000000..5be81ddbf --- /dev/null +++ b/octue/cloud/events/answer_question.py @@ -0,0 +1,38 @@ +import logging + +from octue.cloud.pub_sub.service import Service +from octue.cloud.service_id import create_sruid, get_sruid_parts +from octue.resources.service_backends import GCPPubSubBackend +from octue.runner import Runner + +logger = logging.getLogger(__name__) + + +def answer_question(question, project_name, service_configuration, app_configuration): + """Answer a question received by a service. + + :param dict question: a question event and its attributes + :param str project_name: the name of the project the service is running on + :param octue.configuration.ServiceConfiguration service_configuration: + :param octue.configuration.AppConfiguration app_configuration: + :return dict: the result event + """ + service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) + + service_sruid = create_sruid( + namespace=service_namespace, + name=service_name, + revision_tag=service_revision_tag, + ) + + service = Service(service_id=service_sruid, backend=GCPPubSubBackend(project_name=project_name)) + + runner = Runner.from_configuration( + service_configuration=service_configuration, + app_configuration=app_configuration, + project_name=project_name, + service_id=service_sruid, + ) + + service.run_function = runner.run + return service.answer(question) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py new file mode 100644 index 000000000..059fed713 --- /dev/null +++ b/octue/cloud/events/attributes.py @@ -0,0 +1,305 @@ +import datetime as dt +import uuid as uuid_library + +from octue.definitions import LOCAL_SDK_VERSION +from octue.utils.dictionaries import make_minimal_dictionary + + +class EventAttributes: + """A data structure for holding and working with attributes for a single Octue Twined event. If originator and + parent information aren't provided, the attributes will correspond to an event of any kind related to an originator + question. + + :param str sender: the unique identifier (SRUID) of the service revision sending the question + :param str sender_type: the type of sender for this event; must be one of {"PARENT", "CHILD"} + :param str recipient: the SRUID of the service revision the question is for + :param str|None uuid: the UUID of the event; if `None`, a UUID is generated + :param datetime.datetime|None datetime: the datetime the event was created at; defaults to the current datetime in UTC + :param str|None question_uuid: the UUID of the question; if `None`, a UUID is generated + :param str|None parent_question_uuid: the UUID of the question that triggered this question; this should be `None` if this event relates to the first question in a question tree + :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question; if `None`, the event's related question is assumed to be the originator question and `question_uuid` is used + :param str|None parent: the SRUID of the service revision that asked the question this event is related to + :param str|None originator: the SRUID of the service revision that triggered all ancestor questions of this question; if `None`, the `sender` is used + :param str sender_sdk_version: the semantic version of Octue SDK the sender is running; defaults to the version in the environment + :param int retry_count: the retry count of the question this event is related to (this is zero if it's the first attempt at the question) + :return None: + """ + + def __init__( + self, + sender, + sender_type, + recipient, + uuid=None, + datetime=None, + question_uuid=None, + parent_question_uuid=None, + originator_question_uuid=None, + parent=None, + originator=None, + sender_sdk_version=LOCAL_SDK_VERSION, + retry_count=0, + ): + self.uuid = uuid or str(uuid_library.uuid4()) + self.datetime = datetime or dt.datetime.now(tz=dt.timezone.utc) + self.question_uuid = question_uuid or str(uuid_library.uuid4()) + self.parent_question_uuid = parent_question_uuid + self.originator_question_uuid = originator_question_uuid or self.question_uuid + self.sender = sender + self.parent = parent or self.sender + self.originator = originator or self.sender + self.sender_type = sender_type + self.sender_sdk_version = sender_sdk_version + self.recipient = recipient + self.retry_count = retry_count + + @classmethod + def from_serialised_attributes(cls, serialised_attributes): + """Extract a Twined service event's attributes and deserialise them to the expected form. This function doesn't + assume the required attributes are present as validation hasn't happened yet. + + :param dict serialised_attributes: the event container in dictionary format or direct Google Pub/Sub format + :return octue.cloud.events.attributes.EventAttributes: the extracted and deserialised attributes + """ + serialised_attributes = dict(serialised_attributes) + retry_count = serialised_attributes.get("retry_count") + + if retry_count: + serialised_attributes["retry_count"] = int(retry_count) + + return cls( + uuid=serialised_attributes.get("uuid"), + datetime=serialised_attributes.get("datetime"), + question_uuid=serialised_attributes.get("question_uuid"), + parent_question_uuid=serialised_attributes.get("parent_question_uuid"), + originator_question_uuid=serialised_attributes.get("originator_question_uuid"), + sender=serialised_attributes.get("sender"), + parent=serialised_attributes.get("parent"), + originator=serialised_attributes.get("originator"), + sender_type=serialised_attributes.get("sender_type"), + sender_sdk_version=serialised_attributes.get("sender_sdk_version"), + recipient=serialised_attributes.get("recipient"), + retry_count=serialised_attributes.get("retry_count"), + ) + + def reset_uuid_and_datetime(self): + """Set a new UUID and datetime. This avoids having to create a new instance for every single event (for which + all other attributes are the same). + + :return None: + """ + self.uuid = str(uuid_library.uuid4()) + self.datetime = dt.datetime.now(tz=dt.timezone.utc) + + def to_minimal_dict(self): + """Convert the attributes to a minimal dictionary containing only the attributes that have a non-`None` value. + Using a minimal dictionary means the smallest possible data structure is used so `None` values don't, + for example, need to be redundantly encoded and transmitted when part of a JSON payload for a Pub/Sub message. + + :return dict: the non-`None` attributes + """ + return make_minimal_dictionary( + uuid=self.uuid, + datetime=self.datetime, + question_uuid=self.question_uuid, + parent_question_uuid=self.parent_question_uuid, + originator_question_uuid=self.originator_question_uuid, + parent=self.parent, + originator=self.originator, + sender=self.sender, + sender_type=self.sender_type, + sender_sdk_version=self.sender_sdk_version, + recipient=self.recipient, + retry_count=self.retry_count, + ) + + def to_serialised_attributes(self): + """Convert the attributes to their serialised forms. This is required for e.g. sending the attributes as message + attributes on a Pub/Sub message. A minimal dictionary is produced containing only the attributes that have a + non-`None` value. + + :return dict: the attribute names of the non-`None` attributes mapped to their serialised values + """ + serialised_attributes = {} + + for key, value in self.to_minimal_dict().items(): + if isinstance(value, bool): + value = str(int(value)) + elif isinstance(value, (int, float)): + value = str(value) + elif isinstance(value, dt.datetime): + value = value.isoformat() + + serialised_attributes[key] = value + + return serialised_attributes + + +class QuestionAttributes(EventAttributes): + """A data structure for holding and working with attributes for a single question event. If originator and parent + information aren't provided, the attributes will correspond to an event of any kind related to an originator + question. + + :param str sender: the unique identifier (SRUID) of the service revision sending the question + :param str sender_type: the type of sender for this event; must be one of {"PARENT", "CHILD"} + :param str recipient: the SRUID of the service revision the question is for + :param str|None uuid: the UUID of the event; if `None`, a UUID is generated + :param datetime.datetime|None datetime: the datetime the event was created at; defaults to the current datetime in UTC + :param str|None question_uuid: the UUID of the question; if `None`, a UUID is generated + :param str|None parent_question_uuid: the UUID of the question that triggered this question; this should be `None` if this event relates to the first question in a question tree + :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question; if `None`, the event's related question is assumed to be the originator question and `question_uuid` is used + :param str|None parent: the SRUID of the service revision that asked the question this event is related to + :param str|None originator: the SRUID of the service revision that triggered all ancestor questions of this question; if `None`, the `sender` is used + :param str sender_sdk_version: the semantic version of Octue SDK the sender is running; defaults to the version in the environment + :param int retry_count: the retry count of the question this event is related to (this is zero if it's the first attempt at the question) + :param bool|None forward_logs: if this isn't a `question` event, this should be `None`; otherwise, it should be a boolean indicating whether the parent requested the child to forward its logs to it + :param str|None save_diagnostics: if this isn't a `question` event, this should be `None`; otherwise, it must be one of {"SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"} + :param int|None cpus: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the number of CPUs requested for the question + :param str|None memory: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of memory requested for the question e.g. "256Mi" or "1Gi" + :param str|None ephemeral_storage: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of ephemeral storage requested for the question e.g. "256Mi" or "1Gi" + :return None: + """ + + def __init__( + self, + sender, + sender_type, + recipient, + uuid=None, + datetime=None, + question_uuid=None, + parent_question_uuid=None, + originator_question_uuid=None, + parent=None, + originator=None, + sender_sdk_version=LOCAL_SDK_VERSION, + retry_count=0, + forward_logs=None, + save_diagnostics=None, + cpus=None, + memory=None, + ephemeral_storage=None, + ): + super().__init__( + sender, + sender_type, + recipient, + uuid, + datetime, + question_uuid, + parent_question_uuid, + originator_question_uuid, + parent, + originator, + sender_sdk_version, + retry_count, + ) + + self.forward_logs = forward_logs + self.save_diagnostics = save_diagnostics + self.cpus = cpus + self.memory = memory + self.ephemeral_storage = ephemeral_storage + + @classmethod + def from_serialised_attributes(cls, serialised_attributes): + """Extract and deserialise the attributes specific to a question event. This function doesn't assume these + attributes are present as validation hasn't happened yet. + + :param dict serialised_attributes: attributes for a question event + :return octue.cloud.events.attributes.QuestionAttributes: the deserialised attributes + """ + serialised_attributes = dict(serialised_attributes) + retry_count = serialised_attributes.get("retry_count") + + if retry_count: + serialised_attributes["retry_count"] = int(retry_count) + + forward_logs = serialised_attributes.get("forward_logs") + + if forward_logs: + serialised_attributes["forward_logs"] = bool(int(forward_logs)) + + cpus = serialised_attributes.get("cpus") + + if cpus: + serialised_attributes["cpus"] = int(cpus) + + return cls( + uuid=serialised_attributes.get("uuid"), + datetime=serialised_attributes.get("datetime"), + question_uuid=serialised_attributes.get("question_uuid"), + parent_question_uuid=serialised_attributes.get("parent_question_uuid"), + originator_question_uuid=serialised_attributes.get("originator_question_uuid"), + sender=serialised_attributes.get("sender"), + parent=serialised_attributes.get("parent"), + originator=serialised_attributes.get("originator"), + sender_type=serialised_attributes.get("sender_type"), + sender_sdk_version=serialised_attributes.get("sender_sdk_version"), + recipient=serialised_attributes.get("recipient"), + retry_count=serialised_attributes.get("retry_count"), + forward_logs=serialised_attributes.get("forward_logs"), + save_diagnostics=serialised_attributes.get("save_diagnostics"), + cpus=serialised_attributes.get("cpus"), + memory=serialised_attributes.get("memory"), + ephemeral_storage=serialised_attributes.get("ephemeral_storage"), + ) + + def to_minimal_dict(self): + """Convert the attributes to a minimal dictionary containing only the attributes that have a non-`None` value. + Using a minimal dictionary means the smallest possible data structure is used so `None` values don't, + for example, need to be redundantly encoded and transmitted when part of a JSON payload for a Pub/Sub message. + + :return dict: the non-`None` attributes + """ + return { + **super().to_minimal_dict(), + **make_minimal_dictionary( + forward_logs=self.forward_logs, + save_diagnostics=self.save_diagnostics, + cpus=self.cpus, + memory=self.memory, + ephemeral_storage=self.ephemeral_storage, + ), + } + + +class ResponseAttributes(EventAttributes): + """A data structure for holding and working with attributes for a single response event of any kind. If originator + and parent information aren't provided, the attributes will correspond to an event of any kind related to an + originator question. + + :param str sender: the unique identifier (SRUID) of the service revision sending the question + :param str sender_type: the type of sender for this event; must be one of {"PARENT", "CHILD"} + :param str recipient: the SRUID of the service revision the question is for + :param str|None uuid: the UUID of the event; if `None`, a UUID is generated + :param datetime.datetime|None datetime: the datetime the event was created at; defaults to the current datetime in UTC + :param str|None question_uuid: the UUID of the question; if `None`, a UUID is generated + :param str|None parent_question_uuid: the UUID of the question that triggered this question; this should be `None` if this event relates to the first question in a question tree + :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question; if `None`, the event's related question is assumed to be the originator question and `question_uuid` is used + :param str|None parent: the SRUID of the service revision that asked the question this event is related to + :param str|None originator: the SRUID of the service revision that triggered all ancestor questions of this question; if `None`, the `sender` is used + :param str sender_sdk_version: the semantic version of Octue SDK the sender is running; defaults to the version in the environment + :param int retry_count: the retry count of the question this event is related to (this is zero if it's the first attempt at the question) + :return None: + """ + + @classmethod + def from_question_attributes(cls, question_attributes): + """Create corresponding response attributes from a set of question attributes. + + :param octue.cloud.events.attributes.QuestionEventAttributes question_attributes: the question attributes to make the response attributes from + :return octue.cloud.events.attributes.ResponseEventAttributes: the event attributes for a response event of any kind + """ + return cls( + sender_type="CHILD", + sender=question_attributes.recipient, + recipient=question_attributes.sender, + question_uuid=question_attributes.question_uuid, + parent_question_uuid=question_attributes.parent_question_uuid, + originator_question_uuid=question_attributes.originator_question_uuid, + parent=question_attributes.parent, + originator=question_attributes.originator, + sender_sdk_version=question_attributes.sender_sdk_version, + retry_count=question_attributes.retry_count, + ) diff --git a/octue/cloud/events/handler.py b/octue/cloud/events/handler.py index a8fe55b8f..581461f3b 100644 --- a/octue/cloud/events/handler.py +++ b/octue/cloud/events/handler.py @@ -1,10 +1,9 @@ import abc -import importlib.metadata +from datetime import datetime import logging import os import re import time -from datetime import datetime from octue.cloud import EXCEPTIONS_MAPPING from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA, is_event_valid @@ -12,7 +11,6 @@ from octue.log_handlers import COLOUR_PALETTE from octue.resources.manifest import Manifest - logger = logging.getLogger(__name__) @@ -23,9 +21,6 @@ from octue.utils.colour import colourise -PARENT_SDK_VERSION = importlib.metadata.version("octue") - - class AbstractEventHandler: """An abstract event handler for Octue service events that: - Provide handlers for the Octue service event kinds (see https://strands.octue.com/octue/service-communication) @@ -111,30 +106,20 @@ def _extract_and_validate_event(self, container): """ try: event, attributes = self._extract_event_and_attributes(container) - except Exception: + except Exception as e: + logger.exception(e) event = None attributes = {} - # Don't assume the presence of specific attributes before validation. - recipient = attributes.get("recipient") - child_sdk_version = attributes.get("sender_sdk_version") - if self.validate_events and not is_event_valid( event=event, attributes=attributes, - recipient=recipient, - parent_sdk_version=PARENT_SDK_VERSION, - child_sdk_version=child_sdk_version, + recipient=attributes.recipient, schema=self.schema, ): return (None, None) - logger.debug( - "%r: Received an event related to question %r.", - attributes.get("recipient"), - attributes.get("question_uuid"), - ) - + logger.debug("%r: Received an event related to question %r.", attributes.recipient, attributes.question_uuid) return (event, attributes) def _handle_event(self, event, attributes): @@ -145,7 +130,7 @@ def _handle_event(self, event, attributes): :return dict|None: the output of the event (this should be `None` unless the event is a "result" event) """ if self.record_events: - self.handled_events.append({"event": event, "attributes": attributes}) + self.handled_events.append({"event": event, "attributes": attributes.to_minimal_dict()}) if self.only_handle_result and event["kind"] != "result": return @@ -160,7 +145,7 @@ def _handle_delivery_acknowledgement(self, event, attributes): :param dict attributes: the event's attributes :return None: """ - logger.info("%r's question was delivered at %s.", attributes["recipient"], attributes["datetime"]) + logger.info("%rs question was delivered at %s.", attributes.recipient, attributes.datetime) def _handle_heartbeat(self, event, attributes): """Record the time the heartbeat was received. @@ -173,9 +158,9 @@ def _handle_heartbeat(self, event, attributes): logger.info( "%r: Received a heartbeat from service %r for question %r.", - attributes["recipient"], - attributes["sender"], - attributes["question_uuid"], + attributes.recipient, + attributes.sender, + attributes.question_uuid, ) def _handle_monitor_message(self, event, attributes): @@ -187,9 +172,9 @@ def _handle_monitor_message(self, event, attributes): """ logger.debug( "%r: Received a monitor message from service %r for question %r.", - attributes["recipient"], - attributes["sender"], - attributes["question_uuid"], + attributes.recipient, + attributes.sender, + attributes.question_uuid, ) if self.handle_monitor_message is not None: @@ -217,7 +202,7 @@ def _handle_log_message(self, event, attributes): # Get information about the immediate child sending the event and colour it with the first colour in the # colour palette. immediate_child_analysis_section = colourise( - f"[{attributes['sender']} | {attributes['question_uuid']}]", + f"[{attributes.sender} | {attributes.question_uuid}]", text_colour=self._log_message_colours[0], ) @@ -246,7 +231,7 @@ def _handle_exception(self, event, attributes): exception_message = "\n\n".join( ( event["exception_message"], - f"The following traceback was captured from the remote service {attributes['sender']!r}:", + f"The following traceback was captured from the remote service {attributes.sender!r}:", "".join(event["exception_traceback"]), ) ) @@ -267,7 +252,7 @@ def _handle_result(self, event, attributes): :param dict attributes: the event's attributes :return dict: """ - logger.info("%r: Received an answer to question %r.", attributes["recipient"], attributes["question_uuid"]) + logger.info("%r: Received an answer to question %r.", attributes.recipient, attributes.question_uuid) if event.get("output_manifest"): output_manifest = Manifest.deserialise(event["output_manifest"]) diff --git a/octue/cloud/events/question.py b/octue/cloud/events/question.py new file mode 100644 index 000000000..892306dc0 --- /dev/null +++ b/octue/cloud/events/question.py @@ -0,0 +1,36 @@ +from octue.cloud.events.attributes import QuestionAttributes +from octue.utils.dictionaries import make_minimal_dictionary + + +def make_question_event( + input_values=None, + input_manifest=None, + sender=None, + recipient=None, + question_uuid=None, + attributes=None, +): + """Make a question event. If the `attributes` argument isn't provided, the question will be an originator question. + + :param dict|None input_values: any input values for the question + :param dict|None input_manifest: an input manifest of any datasets needed for the question as a python primitive + :param str|None sender: the service revision unique identifier (SRUID) of the service revision sending the question + :param str|None recipient: the service revision unique identifier (SRUID) of the service revision the question is for + :param str|None question_uuid: the UUID to use for the question; if `None`, a UUID is generated + :param dict|None attributes: the attributes to use for the question event; if none are provided, the question will be an originator question + :return dict: the question event and its attributes + """ + if not attributes: + attributes = QuestionAttributes( + question_uuid=question_uuid, + sender=sender, + recipient=recipient, + forward_logs=True, + save_diagnostics="SAVE_DIAGNOSTICS_ON_CRASH", + sender_type="PARENT", + ).to_minimal_dict() + + return { + "event": make_minimal_dictionary(input_values=input_values, input_manifest=input_manifest, kind="question"), + "attributes": attributes, + } diff --git a/octue/cloud/events/replayer.py b/octue/cloud/events/replayer.py index 8336bf2c7..ab57f6003 100644 --- a/octue/cloud/events/replayer.py +++ b/octue/cloud/events/replayer.py @@ -1,9 +1,9 @@ import logging +from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA - logger = logging.getLogger(__name__) @@ -78,13 +78,15 @@ def handle_events(self, events): if result: return result + logger.warning("No result was found for this question.") + def _extract_event_and_attributes(self, container): """Extract an event and its attributes from the event container. :param dict container: the container of the event :return (any, dict): the event and its attributes """ - return container["event"], container["attributes"] + return container.get("event", {}), ResponseAttributes(**container["attributes"]) def _handle_question(self, event, attributes): """Log that the question was sent. diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index 41df484db..f625994a7 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -3,7 +3,7 @@ import jsonschema from octue.compatibility import warn_if_incompatible - +from octue.definitions import LOCAL_SDK_VERSION VALID_EVENT_KINDS = { "question", @@ -15,14 +15,13 @@ "result", } -SERVICE_COMMUNICATION_SCHEMA_VERSION = "0.14.1" +SERVICE_COMMUNICATION_SCHEMA_VERSION = "0.15.0" SERVICE_COMMUNICATION_SCHEMA_INFO_URL = "https://strands.octue.com/octue/service-communication" SERVICE_COMMUNICATION_SCHEMA = { "$ref": f"https://jsonschema.registry.octue.com/octue/service-communication/{SERVICE_COMMUNICATION_SCHEMA_VERSION}.json" } - # Instantiate a JSON schema validator to cache the service communication schema. This avoids downloading it from the # registry every time a message is validated against it. jsonschema.Draft202012Validator.check_schema(SERVICE_COMMUNICATION_SCHEMA) @@ -31,46 +30,35 @@ logger = logging.getLogger(__name__) -def is_event_valid(event, attributes, recipient, parent_sdk_version, child_sdk_version, schema=None): +def is_event_valid(event, attributes, recipient, schema=None): """Check if the event and its attributes are valid according to the Octue services communication schema. :param dict event: the event to validate - :param dict attributes: the attributes of the event to validate + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes of the event to validate :param str recipient: the SRUID of the service revision receiving and validating the event - :param str parent_sdk_version: the semantic version of Octue SDK running on the parent - :param str child_sdk_version: the semantic version of Octue SDK running on the child :param dict|None schema: the schema to validate the event and its attributes against; if `None`, this defaults to the service communication schema used in this version of Octue SDK :return bool: `True` if the event and its attributes are valid """ try: - raise_if_event_is_invalid( - event, - attributes, - recipient, - parent_sdk_version, - child_sdk_version, - schema=schema, - ) + raise_if_event_is_invalid(event, attributes, recipient, schema=schema) except jsonschema.ValidationError: return False return True -def raise_if_event_is_invalid(event, attributes, recipient, parent_sdk_version, child_sdk_version, schema=None): +def raise_if_event_is_invalid(event, attributes, recipient, schema=None): """Raise an error if the event or its attributes aren't valid according to the Octue services communication schema. :param dict event: the event to validate - :param dict attributes: the attributes of the event to validate + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes of the event to validate :param str recipient: the SRUID of the service revision receiving and validating the event - :param str parent_sdk_version: the semantic version of Octue SDK running on the parent - :param str child_sdk_version: the semantic version of Octue SDK running on the child :param dict|None schema: the schema to validate the event and its attributes against; if `None`, this defaults to the service communication schema used in this version of Octue SDK :raise jsonschema.ValidationError: if the event or its attributes are invalid :return None: """ # Transform attributes to a dictionary in the case they're a different kind of mapping. - data = {"event": event, "attributes": dict(attributes)} + data = {"event": event, "attributes": attributes.to_minimal_dict()} if schema is None: schema = SERVICE_COMMUNICATION_SCHEMA @@ -85,7 +73,7 @@ def raise_if_event_is_invalid(event, attributes, recipient, parent_sdk_version, jsonschema.validate(data, schema) except jsonschema.ValidationError as error: - warn_if_incompatible(parent_sdk_version=parent_sdk_version, child_sdk_version=child_sdk_version) + warn_if_incompatible(sender_sdk_version=attributes.sender_sdk_version, recipient_sdk_version=LOCAL_SDK_VERSION) logger.exception( "%r received an event that doesn't conform with version %s of the service communication schema (%s): %r.", diff --git a/octue/cloud/pub_sub/__init__.py b/octue/cloud/pub_sub/__init__.py index 30c21aed7..677e54157 100644 --- a/octue/cloud/pub_sub/__init__.py +++ b/octue/cloud/pub_sub/__init__.py @@ -1,44 +1,4 @@ -from octue.cloud.events import OCTUE_SERVICES_PREFIX -from octue.cloud.service_id import convert_service_id_to_pub_sub_form - from .subscription import Subscription from .topic import Topic - __all__ = ["Subscription", "Topic"] - - -def create_push_subscription( - project_name, - sruid, - push_endpoint, - subscription_filter=None, - expiration_time=None, - allow_existing=True, -): - """Create a Google Pub/Sub push subscription for an Octue service for it to receive questions from parents. If a - corresponding topic doesn't exist, it will be created first. - - :param str project_name: the name of the Google Cloud project in which the subscription will be created - :param str sruid: the SRUID (service revision unique identifier) - :param str push_endpoint: the HTTP/HTTPS endpoint of the service to push to. It should be fully formed and include the 'https://' prefix - :param str|None subscription_filter: if specified, the filter to apply to the subscription; otherwise, no filter is applied - :param float|None expiration_time: the number of seconds of inactivity after which the subscription should expire. If not provided, no expiration time is applied to the subscription - :param bool allow_existing: if True, don't raise an error if the subscription already exists - :return octue.cloud.pub_sub.subscription.Subscription: - """ - if expiration_time: - expiration_time = float(expiration_time) - else: - expiration_time = None - - subscription = Subscription( - name=convert_service_id_to_pub_sub_form(sruid), - topic=Topic(name=OCTUE_SERVICES_PREFIX, project_name=project_name), - filter=subscription_filter, - expiration_time=expiration_time, - push_endpoint=push_endpoint, - ) - - subscription.create(allow_existing=allow_existing) - return subscription diff --git a/octue/cloud/pub_sub/bigquery.py b/octue/cloud/pub_sub/bigquery.py index 8aa809b33..79f769738 100644 --- a/octue/cloud/pub_sub/bigquery.py +++ b/octue/cloud/pub_sub/bigquery.py @@ -1,7 +1,10 @@ +import logging + from google.cloud.bigquery import Client, QueryJobConfig, ScalarQueryParameter from octue.cloud.events.validation import VALID_EVENT_KINDS +logger = logging.getLogger(__name__) DEFAULT_FIELDS = ( "`originator_question_uuid`", @@ -20,11 +23,13 @@ "`other_attributes`", ) +DEFAULT_EVENT_STORE_TABLE_ID = "octue_twined.service-events" + BACKEND_METADATA_FIELDS = ("`backend`", "`backend_metadata`") def get_events( - table_id, + table_id=DEFAULT_EVENT_STORE_TABLE_ID, question_uuid=None, parent_question_uuid=None, originator_question_uuid=None, @@ -44,7 +49,7 @@ def get_events( When the limit is smaller than the total number of events, the default behaviour is to return the "tail" of the event stream for the question (the most recent n events for the question). - :param str table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-project.your-dataset.your-table" + :param str table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-dataset.your-table" :param str|None question_uuid: the UUID of a question to get events for :param str|None parent_question_uuid: the UUID of a parent question to get the sub-question events for :param str|None originator_question_uuid: the UUID of an originator question get the full tree of events for @@ -120,6 +125,7 @@ def get_events( result = query_job.result() if result.total_rows == 0: + logger.warning("No events were found for this question.") return [] events = [_deserialise_event(event) for event in result] diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index cbb021073..e33c8a3b1 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -1,64 +1,37 @@ -import base64 +from datetime import datetime, timedelta +from functools import cached_property import json import logging import time -from datetime import datetime, timedelta -from functools import cached_property from google.api_core import retry from google.cloud.pubsub_v1 import SubscriberClient +from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA +from octue.definitions import DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL from octue.utils.decoders import OctueJSONDecoder from octue.utils.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer - logger = logging.getLogger(__name__) MAX_SIMULTANEOUS_MESSAGES_PULL = 50 -def extract_event_and_attributes_from_pub_sub_message(message): - """Extract an Octue service event and its attributes from a Google Pub/Sub message in either direct Pub/Sub format - or in the Google Cloud Run format. +def extract_event(message): + """Extract a Twined service event from a dictionary or Pub/Sub message. - :param dict|google.cloud.pubsub_v1.subscriber.message.Message message: the message in Google Cloud Run format or Google Pub/Sub format - :return (any, dict): the extracted event and its attributes + :param dict|google.cloud.pubsub_v1.subscriber.message.Message message: the message in dictionary format or direct Google Pub/Sub format + :return dict: the extracted event """ - # Cast attributes to a dictionary to avoid defaultdict-like behaviour from Pub/Sub message attributes container. - attributes = dict(get_nested_attribute(message, "attributes")) - - # Deserialise the `parent_question_uuid`, `forward_logs`, and `retry_count`, fields if they're present - # (don't assume they are before validation). - if attributes.get("parent_question_uuid") == "null": - attributes["parent_question_uuid"] = None - - retry_count = attributes.get("retry_count") - - if retry_count: - attributes["retry_count"] = int(retry_count) - else: - attributes["retry_count"] = None - - # Required for question events. - if attributes.get("sender_type") == "PARENT": - forward_logs = attributes.get("forward_logs") - - if forward_logs: - attributes["forward_logs"] = bool(int(forward_logs)) - else: - attributes["forward_logs"] = None - - try: - # Parse event directly from Pub/Sub or Dataflow. - event = json.loads(message.data.decode(), cls=OctueJSONDecoder) - except Exception: - # Parse event from Google Cloud Run. - event = json.loads(base64.b64decode(message["data"]).decode("utf-8").strip(), cls=OctueJSONDecoder) + # Support already-extracted questions (e.g. from the `octue question ask local` CLI command). + if isinstance(message, dict) and "event" in message: + return message["event"] - return event, attributes + # Extract event directly from Pub/Sub. + return json.loads(message.data.decode(), cls=OctueJSONDecoder) class GoogleCloudPubSubEventHandler(AbstractEventHandler): @@ -138,7 +111,7 @@ def _time_since_last_heartbeat(self): return datetime.now() - self._last_heartbeat - def handle_events(self, timeout=60, maximum_heartbeat_interval=300): + def handle_events(self, timeout=60, maximum_heartbeat_interval=DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL): """Pull events from the subscription and handle them in the order they were sent until a "result" event is handled, then return the handled result. @@ -274,4 +247,7 @@ def _extract_event_and_attributes(self, container): :param dict container: a Pub/Sub message :return (any, dict): the event and its attributes """ - return extract_event_and_attributes_from_pub_sub_message(container.message) + event = extract_event(container.message) + attributes = get_nested_attribute(container.message, "attributes") + attributes = ResponseAttributes.from_serialised_attributes(attributes) + return event, attributes diff --git a/octue/cloud/pub_sub/logging.py b/octue/cloud/pub_sub/logging.py index 0da172017..94fd72e47 100644 --- a/octue/cloud/pub_sub/logging.py +++ b/octue/cloud/pub_sub/logging.py @@ -1,7 +1,6 @@ import logging import re - ANSI_ESCAPE_SEQUENCES_PATTERN = r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])" @@ -9,39 +8,14 @@ class GoogleCloudPubSubHandler(logging.Handler): """A log handler that publishes log records to a Google Cloud Pub/Sub topic. :param callable event_emitter: the `_emit_event` method of the service that instantiated this instance - :param str question_uuid: the UUID of the question to handle log records for - :param str|None parent_question_uuid: the UUID of the question these log records are related to - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the parent that asked the question these log records are related to - :param str originator: the SRUID of the service revision that triggered the tree of questions these log records are related to - :param str recipient: the SRUID of the service to send these log records to - :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the log record event :param float timeout: timeout in seconds for attempting to publish each log record :return None: """ - def __init__( - self, - event_emitter, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - recipient, - retry_count, - timeout=60, - *args, - **kwargs, - ): + def __init__(self, event_emitter, attributes, timeout=60, *args, **kwargs): super().__init__(*args, **kwargs) - self.question_uuid = question_uuid - self.parent_question_uuid = parent_question_uuid - self.originator_question_uuid = originator_question_uuid - self.parent = parent - self.originator = originator - self.recipient = recipient - self.retry_count = retry_count + self.attributes = attributes self.timeout = timeout self._emit_event = event_emitter @@ -57,21 +31,15 @@ def emit(self, record): "kind": "log_record", "log_record": self._convert_log_record_to_primitives(record), }, - parent=self.parent, - originator=self.originator, - recipient=self.recipient, - retry_count=self.retry_count, - question_uuid=self.question_uuid, - parent_question_uuid=self.parent_question_uuid, - originator_question_uuid=self.originator_question_uuid, - # The sender type is repeated here as a string to avoid a circular import. - attributes={"sender_type": "CHILD"}, + attributes=self.attributes, + wait=False, ) except Exception: # noqa self.handleError(record) - def _convert_log_record_to_primitives(self, log_record): + @staticmethod + def _convert_log_record_to_primitives(log_record): """Convert a log record to JSON-serialisable primitives by interpolating the args into the message, and removing the exception info, which is potentially not JSON-serialisable. This is similar to the approach in `logging.handlers.SocketHandler.makePickle`. Also strip any ANSI escape sequences from the message. diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 9d65d5633..da63e8470 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -1,46 +1,41 @@ import concurrent.futures import copy -import datetime import functools -import importlib.metadata import json import logging -import os -import time import uuid -import google.api_core.exceptions -import jsonschema from google.api_core import retry +import google.api_core.exceptions from google.cloud import pubsub_v1 +import jsonschema -import octue.exceptions -from octue.cloud.events import OCTUE_SERVICES_PREFIX +from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME +from octue.cloud.events.attributes import QuestionAttributes, ResponseAttributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic -from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event_and_attributes_from_pub_sub_message +from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler +from octue.cloud.registry import get_default_sruid, raise_if_revision_not_registered from octue.cloud.service_id import ( convert_service_id_to_pub_sub_form, create_sruid, - get_default_sruid, - raise_if_revision_not_registered, split_service_id, validate_sruid, ) from octue.compatibility import warn_if_incompatible +from octue.definitions import DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL, LOCAL_SDK_VERSION +import octue.exceptions from octue.utils.dictionaries import make_minimal_dictionary from octue.utils.encoders import OctueJSONEncoder from octue.utils.exceptions import convert_exception_to_primitives +from octue.utils.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer - logger = logging.getLogger(__name__) -DEFAULT_NAMESPACE = "default" ANSWERS_NAMESPACE = "answers" -OCTUE_SERVICE_REGISTRY_ENDPOINT = "services.registry.octue.com" # Switch message batching off by setting `max_messages` to 1. This minimises latency and is recommended for # microservices publishing single messages in a request-response sequence. @@ -63,14 +58,13 @@ class Service: :param octue.resources.service_backends.ServiceBackend backend: the object representing the type of backend the service uses :param str|None service_id: a unique ID to give to the service (any string); a UUID is generated if none is given :param callable|None run_function: the function the service should run when it is called - :param str|None name: an optional name to use for the service to override its ID in its string representation :param iter(dict)|None service_registries: the names and endpoints of the registries used to resolve service revisions when asking questions; these should be in priority order (highest priority first) :return None: """ def __init__(self, backend, service_id=None, run_function=None, service_registries=None): if service_id is None: - self.id = create_sruid(namespace=DEFAULT_NAMESPACE, name=str(uuid.uuid4())) + self.id = create_sruid() # Raise an error if the service ID is some kind of falsey object that isn't `None`. elif not service_id: @@ -85,7 +79,6 @@ def __init__(self, backend, service_id=None, run_function=None, service_registri self.service_registries = service_registries self._pub_sub_id = convert_service_id_to_pub_sub_form(self.id) - self._local_sdk_version = importlib.metadata.version("octue") self._event_handler = None def __repr__(self): @@ -117,7 +110,7 @@ def services_topic(self): :raise octue.exceptions.ServiceNotFound: if the topic doesn't exist in the project :return octue.cloud.pub_sub.topic.Topic: the Octue services topic for the project """ - topic = Topic(name=OCTUE_SERVICES_PREFIX, project_name=self.backend.project_name) + topic = Topic(name=OCTUE_SERVICES_TOPIC_NAME, project_name=self.backend.project_name) if not topic.exists(): raise octue.exceptions.ServiceNotFound( @@ -203,93 +196,65 @@ def answer(self, question, heartbeat_interval=120, timeout=30): :param int|float heartbeat_interval: the time interval, in seconds, at which to send heartbeats :param float|None timeout: time in seconds to keep retrying sending of the answer once it has been calculated :raise Exception: if any exception arises during running analysis and sending its results - :return None: + :return dict: the result event """ try: - ( - question, - question_uuid, - parent_question_uuid, - originator_question_uuid, - forward_logs, - parent_sdk_version, - save_diagnostics, - parent, - originator, - retry_count, - ) = self._parse_question(question) + question, question_attributes = self._parse_question(question) except jsonschema.ValidationError: return heartbeater = None - - routing_metadata = { - "question_uuid": question_uuid, - "parent_question_uuid": parent_question_uuid, - "originator_question_uuid": originator_question_uuid, - "parent": parent, - "originator": originator, - "retry_count": retry_count, - } + response_attributes = ResponseAttributes.from_question_attributes(question_attributes) try: - self._send_delivery_acknowledgment(**routing_metadata) - start_time = time.perf_counter() + self._send_delivery_acknowledgment(response_attributes) heartbeater = RepeatingTimer( interval=heartbeat_interval, - function=self._send_heartbeat_and_check_runtime, - kwargs={"start_time": start_time, **routing_metadata}, + function=self._send_heartbeat, + kwargs={"attributes": response_attributes}, ) heartbeater.daemon = True heartbeater.start() - if forward_logs: + if question_attributes.forward_logs: analysis_log_handler = GoogleCloudPubSubHandler( event_emitter=self._emit_event, - recipient=parent, - **routing_metadata, + attributes=response_attributes, ) else: analysis_log_handler = None - handle_monitor_message = functools.partial(self._send_monitor_message, **routing_metadata) + handle_monitor_message = functools.partial(self._send_monitor_message, attributes=response_attributes) analysis = self.run_function( - analysis_id=question_uuid, + analysis_id=question_attributes.question_uuid, input_values=question.get("input_values"), input_manifest=question.get("input_manifest"), children=question.get("children"), analysis_log_handler=analysis_log_handler, handle_monitor_message=handle_monitor_message, - save_diagnostics=save_diagnostics, - originator_question_uuid=originator_question_uuid, - originator=originator, - ) - - result = make_minimal_dictionary(kind="result", output_values=analysis.output_values) - - if analysis.output_manifest is not None: - result["output_manifest"] = analysis.output_manifest.to_primitive() - - self._emit_event( - event=result, - recipient=parent, - attributes={"sender_type": CHILD_SENDER_TYPE}, - timeout=timeout, - **routing_metadata, + save_diagnostics=question_attributes.save_diagnostics, + originator_question_uuid=question_attributes.originator_question_uuid, + originator=question_attributes.originator, ) + result = self._send_result(analysis, response_attributes) heartbeater.cancel() - logger.info("%r answered question %r.", self, question_uuid) + logger.info("%r answered question %r.", self, question_attributes.question_uuid) + return result except BaseException as error: # noqa if heartbeater is not None: heartbeater.cancel() - warn_if_incompatible(child_sdk_version=self._local_sdk_version, parent_sdk_version=parent_sdk_version) - self.send_exception(timeout=timeout, **routing_metadata) + warn_if_incompatible( + recipient_sdk_version=LOCAL_SDK_VERSION, + sender_sdk_version=question_attributes.sender_sdk_version, + ) + + self.send_exception(attributes=response_attributes, timeout=timeout) raise error def ask( @@ -308,6 +273,9 @@ def ask( push_endpoint=None, asynchronous=False, retry_count=0, + cpus=None, + memory=None, + ephemeral_storage=None, timeout=86400, ): """Ask a child a question (i.e. send it input values for it to analyse and produce output values for) and return @@ -316,7 +284,7 @@ def ask( :param str service_id: the ID of the child to ask the question to :param any|None input_values: any input values for the question - :param octue.resources.manifest.Manifest|None input_manifest: an input manifest of any datasets needed for the question + :param dict|octue.resources.manifest.Manifest|None input_manifest: an input manifest of any datasets needed for the question :param list(dict)|None children: a list of children for the child to use instead of its default children (if it uses children). These should be in the same format as in an app's app configuration file and have the same keys. :param bool subscribe_to_logs: if `True`, subscribe to the child's logs and handle them with the local log handlers :param bool allow_local_files: if `True`, allow the input manifest to contain references to local files - this should only be set to `True` if the child will be able to access these local files @@ -328,6 +296,9 @@ def ask( :param str|None push_endpoint: if answers to the question should be pushed to an endpoint, provide its URL here (the returned subscription will be a push subscription); if not, leave this as `None` :param bool asynchronous: if `True` and not using a push endpoint, don't create an answer subscription :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param int|None cpus: the number of CPUs to request for the question; defaults to the number set by the child service + :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service + :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service :param float|None timeout: time in seconds to keep retrying sending the question :return (octue.cloud.pub_sub.subscription.Subscription|None, str): the answer subscription (if the question is synchronous or a push endpoint was used) and question UUID """ @@ -345,17 +316,7 @@ def ask( service_registries=self.service_registries, ) - # If not using a service registry, check that the service revision exists by checking for its subscription. - elif service_revision_tag: - service_revision_subscription = Subscription( - name=convert_service_id_to_pub_sub_form(service_id), - topic=self.services_topic, - ) - - if not service_revision_subscription.exists(): - raise octue.exceptions.ServiceNotFound(f"Service revision {service_id!r} not found.") - - else: + elif not service_revision_tag: raise octue.exceptions.InvalidServiceID( f"A service revision tag for {service_id!r} must be provided if service registries aren't being used." ) @@ -408,6 +369,9 @@ def ask( originator_question_uuid=originator_question_uuid, originator=originator, recipient=service_id, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, retry_count=retry_count, ) @@ -419,7 +383,7 @@ def wait_for_answer( handle_monitor_message=None, record_events=True, timeout=60, - maximum_heartbeat_interval=300, + maximum_heartbeat_interval=DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL, ): """Wait for an answer to a question on the given subscription, deleting the subscription and its topic once the answer is received. @@ -453,24 +417,40 @@ def wait_for_answer( finally: subscription.delete() - def send_exception( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + # def cancel(self, question_uuid, event_store_table_id, timeout=30): + # """Request cancellation of a running question. + # + # :param str question_uuid: the question UUID of the question to cancel + # :param str event_store_table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-project.your-dataset.your-table" + # :param float timeout: time to wait for the cancellation to send before raising a timeout error [s] + # :raise ValueError: if no question or more than one question is found for the given question UUID + # :return None: + # """ + # questions = get_events(table_id=event_store_table_id, question_uuid=question_uuid, kinds=["question"]) + # + # if len(questions) == 0: + # raise ValueError(f"No question found with question UUID {question_uuid!r}.") + # + # if len(questions) > 1: + # raise ValueError(f"Multiple questions found with same question UUID {question_uuid!r}.") + # + # question_finished = get_events( + # table_id=event_store_table_id, + # question_uuid=question_uuid, + # kinds=["result", "exception"], + # ) + # + # if question_finished: + # logger.warning("Cannot cancel question %r - it has already finished.", question_uuid) + # + # question_attributes = EventAttributes(**questions[0]["attributes"]) + # self._emit_event({"kind": "cancellation"}, attributes=question_attributes, timeout=timeout) + # logger.info("Cancellation of question %r requested.", question_uuid) + + def send_exception(self, attributes, timeout=30): """Serialise and send the exception being handled to the parent. - :param str question_uuid: the UUID of the question this event relates to - :param str|None parent_question_uuid: the UUID of the question that triggered this question - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the parent that asked the question this event is related to - :param str originator: the SRUID of the service revision that triggered all ancestor questions of this question - :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the exception event :param float|None timeout: time in seconds to keep retrying sending of the exception :return None: """ @@ -484,30 +464,11 @@ def send_exception( "exception_message": exception_message, "exception_traceback": exception["traceback"], }, - question_uuid=question_uuid, - parent_question_uuid=parent_question_uuid, - originator_question_uuid=originator_question_uuid, - parent=parent, - originator=originator, - recipient=parent, - retry_count=retry_count, - attributes={"sender_type": CHILD_SENDER_TYPE}, + attributes=attributes, timeout=timeout, ) - def _emit_event( - self, - event, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - recipient, - retry_count, - attributes=None, - timeout=30, - ): + def _emit_event(self, event, attributes, wait=True, timeout=30): """Emit a JSON-serialised event as a Pub/Sub message to the services topic with optional message attributes. Extra attributes can be added to an event via the `attributes` argument but the following attributes are always included: @@ -524,55 +485,24 @@ def _emit_event( - `datetime` :param dict event: JSON-serialisable data to emit as an event - :param str question_uuid: - :param str|None parent_question_uuid: the UUID of the question that triggered this question - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the parent that asked the question this event is related to - :param str originator: the SRUID of the service revision that triggered all ancestor questions of this question - :param str recipient: the SRUID of the service the event is intended for - :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) - :param dict|None attributes: key-value pairs to attach to the event - the values must be strings or bytes + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the event + :param bool wait: if `True`, wait for the result of the publishing future before continuing execution (this is important if the python process ends promptly after the event is emitted instead of being part of a prolonged stream as the publishing may not complete and the event won't actually be emitted) :param int|float timeout: the timeout for sending the event in seconds :return google.cloud.pubsub_v1.publisher.futures.Future: """ - attributes = attributes or {} - - attributes.update( - { - "uuid": str(uuid.uuid4()), - "datetime": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), - "question_uuid": question_uuid, - "parent_question_uuid": parent_question_uuid, - "originator_question_uuid": originator_question_uuid, - "parent": parent, - "originator": originator, - "sender": self.id, - "sender_sdk_version": self._local_sdk_version, - "recipient": recipient, - "retry_count": retry_count, - } - ) - - converted_attributes = {} - - for key, value in attributes.items(): - if isinstance(value, bool): - value = str(int(value)) - elif isinstance(value, (int, float)): - value = str(value) - elif value is None: - value = json.dumps(value) - - converted_attributes[key] = value + attributes.reset_uuid_and_datetime() future = self.publisher.publish( topic=self.services_topic.path, data=json.dumps(event, cls=OctueJSONEncoder).encode(), - ordering_key=question_uuid, + ordering_key=attributes.question_uuid, retry=retry.Retry(deadline=timeout), - **converted_attributes, + **attributes.to_serialised_attributes(), ) + if wait: + future.result() + return future def _send_question( @@ -588,6 +518,9 @@ def _send_question( originator, recipient, retry_count, + cpus, + memory, + ephemeral_storage, timeout=30, ): """Send a question to a child service. @@ -612,186 +545,94 @@ def _send_question( input_manifest.use_signed_urls_for_datasets() question["input_manifest"] = input_manifest.to_primitive() - future = self._emit_event( - event=question, + question_attributes = QuestionAttributes( question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, originator_question_uuid=originator_question_uuid, parent=self.id, originator=originator, + sender=self.id, + sender_type=PARENT_SENDER_TYPE, recipient=recipient, retry_count=retry_count, - attributes={ - "forward_logs": forward_logs, - "save_diagnostics": save_diagnostics, - "sender_type": PARENT_SENDER_TYPE, - }, - timeout=timeout, + forward_logs=forward_logs, + save_diagnostics=save_diagnostics, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, ) - # Await successful publishing of the question. - future.result() + self._emit_event(event=question, attributes=question_attributes, timeout=timeout) logger.info("%r asked a question %r to service %r.", self, question_uuid, recipient) - def _send_delivery_acknowledgment( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + def _send_delivery_acknowledgment(self, attributes, timeout=30): """Send an acknowledgement of question receipt to the parent. - :param str question_uuid: the UUID of the question this event relates to - :param str|None parent_question_uuid: the UUID of the question that triggered this question - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the service that asked the question this event is related to - :param str originator: the SRUID of the service revision that triggered all ancestor questions of this question - :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the delivery acknowledgement event :param float timeout: time in seconds after which to give up sending :return None: """ - self._emit_event( - {"kind": "delivery_acknowledgement"}, - question_uuid=question_uuid, - parent_question_uuid=parent_question_uuid, - originator_question_uuid=originator_question_uuid, - timeout=timeout, - parent=parent, - originator=originator, - recipient=parent, - retry_count=retry_count, - attributes={"sender_type": CHILD_SENDER_TYPE}, - ) - - logger.info("%r acknowledged receipt of question %r.", self, question_uuid) + self._emit_event({"kind": "delivery_acknowledgement"}, attributes=attributes, timeout=timeout, wait=False) + logger.info("%r acknowledged receipt of question %r.", self, attributes.question_uuid) - def _send_heartbeat_and_check_runtime( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - start_time, - runtime_timeout_warning_time=3480, # This is 58 minutes in seconds. - timeout=30, - ): - """Send a heartbeat to the parent, indicating that the service is alive. If it's running on Cloud Run and it's - been running for longer than the runtime timeout warning time, log a warning that it will be stopped soon. + def _send_heartbeat(self, attributes, timeout=30): + """Send a heartbeat to the parent, indicating that the service is alive. - :param str question_uuid: the UUID of the question this event relates to - :param str|None parent_question_uuid: the UUID of the question that triggered this question - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the parent that asked the question this event is related to - :param str originator: the SRUID of the service revision that triggered all ancestor questions of this question - :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) - :param int|float start_time: the `time.perf_counter` time that the analysis was started [s] - :param int|float runtime_timeout_warning_time: the amount of time after which to warn that the runtime timeout is approaching [s] + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the heartbeat event :param float timeout: time in seconds after which to give up sending :return None: """ - self._emit_event( - {"kind": "heartbeat"}, - question_uuid=question_uuid, - parent_question_uuid=parent_question_uuid, - originator_question_uuid=originator_question_uuid, - parent=parent, - originator=originator, - recipient=parent, - retry_count=retry_count, - attributes={"sender_type": CHILD_SENDER_TYPE}, - timeout=timeout, - ) - - if ( - os.environ.get("COMPUTE_PROVIDER") == "GOOGLE_CLOUD_RUN" - and time.perf_counter() - start_time > runtime_timeout_warning_time - ): - logger.warning("This analysis will reach the maximum runtime and be stopped soon.") - + self._emit_event({"kind": "heartbeat"}, attributes=attributes, timeout=timeout, wait=False) logger.debug("Heartbeat sent by %r.", self) - def _send_monitor_message( - self, - data, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + def _send_monitor_message(self, data, attributes, timeout=30): """Send a monitor message to the parent. :param any data: the data to send as a monitor message - :param str question_uuid: the UUID of the question this event relates to - :param str|None parent_question_uuid: the UUID of the question that triggered this question - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the service that asked the question this event is related to - :param str originator: the SRUID of the service revision that triggered all ancestor questions of this question - :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the monitor message event :param float timeout: time in seconds to retry sending the message :return None: """ - self._emit_event( - {"kind": "monitor_message", "data": data}, - question_uuid=question_uuid, - parent_question_uuid=parent_question_uuid, - originator_question_uuid=originator_question_uuid, - parent=parent, - originator=originator, - recipient=parent, - retry_count=retry_count, - timeout=timeout, - attributes={"sender_type": CHILD_SENDER_TYPE}, - ) - + self._emit_event({"kind": "monitor_message", "data": data}, attributes=attributes, timeout=timeout, wait=False) logger.debug("Monitor message sent by %r.", self) + def _send_result(self, analysis, attributes, timeout=30): + """Send the result to the parent. + + :param octue.resources.analysis.Analysis analysis: the analysis object containing the output values and/or output manifest + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the result event + :param float timeout: time in seconds to retry sending the message + :return dict: the result + """ + result = make_minimal_dictionary(kind="result", output_values=analysis.output_values) + + if analysis.output_manifest is not None: + result["output_manifest"] = analysis.output_manifest.to_primitive() + + self._emit_event(event=result, attributes=attributes, timeout=timeout) + return result + def _parse_question(self, question): - """Parse a question in the Google Cloud Run or Google Pub/Sub format. + """Parse a question in dictionary format or direct Google Pub/Sub format. - :param dict|google.cloud.pubsub_v1.subscriber.message.Message question: the question to parse in Google Cloud Run or Google Pub/Sub format - :return (dict, str, str, str, bool, str, str, str, str, int): the question's event and its attributes (question UUID, parent question UUID, originator question UUID, whether to forward logs, the Octue SDK version of the parent, whether to save diagnostics, the SRUID of the parent that asked the question, the SRUID of the service revision that triggered all ancestor questions of this question, and the retry count) + :param dict|google.cloud.pubsub_v1.subscriber.message.Message question: the question to parse in dictionary format or direct Google Pub/Sub format + :return (dict, octue.cloud.events.attributes.QuestionAttributes): the question's event and its attributes """ logger.info("%r received a question.", self) # Acknowledge the question if it's directly from Pub/Sub. if hasattr(question, "ack"): question.ack() + logger.info("Question acknowledged on Pub/Sub.") - event, attributes = extract_event_and_attributes_from_pub_sub_message(question) - event_for_validation = copy.deepcopy(event) + event = extract_event(question) + attributes = QuestionAttributes.from_serialised_attributes(get_nested_attribute(question, "attributes")) + logger.info("Extracted question event and attributes.") - raise_if_event_is_invalid( - event=event_for_validation, - attributes=attributes, - recipient=self.id, - # Don't assume the presence of specific attributes before validation. - parent_sdk_version=attributes.get("sender_sdk_version"), - child_sdk_version=importlib.metadata.version("octue"), - ) + raise_if_event_is_invalid(event=copy.deepcopy(event), attributes=attributes, recipient=self.id) + logger.info("%r parsed question %r successfully.", self, attributes.question_uuid) - logger.info("%r parsed question %r successfully.", self, attributes["question_uuid"]) - - if attributes["retry_count"] > 0: - logger.warning("This is retry %d for question %r.", attributes["retry_count"], attributes["question_uuid"]) - - return ( - event, - attributes["question_uuid"], - attributes["parent_question_uuid"], - attributes["originator_question_uuid"], - attributes["forward_logs"], - attributes["sender_sdk_version"], - attributes["save_diagnostics"], - attributes["parent"], - attributes["originator"], - attributes["retry_count"], - ) + if attributes.retry_count > 0: + logger.warning("This is retry %d for question %r.", attributes.retry_count, attributes.question_uuid) + + return event, attributes diff --git a/octue/cloud/registry.py b/octue/cloud/registry.py new file mode 100644 index 000000000..9f3d5b00a --- /dev/null +++ b/octue/cloud/registry.py @@ -0,0 +1,95 @@ +import google.auth +import google.oauth2 +import requests + +from octue.cloud.service_id import create_sruid, logger, split_service_id +import octue.exceptions + + +def get_default_sruid(namespace, name, service_registries): + """Get the SRUID of the default revision of the service `/` if it exists in one of the specified + service registries. The registries should be provided in priority order so that, if more than one registry contains + a matching service, the revision that's returned is taken from the highest priority (first) registry. + + :param str namespace: the namespace of the service + :param str name: the name of the service + :param iter(dict) service_registries: the registries to look for the service in; the registries should be in priority order in case more than one has a service with the given namespace and name + :raise octue.exceptions.ServiceNotFound: if a revision can't be found for the service in the service registries + :return str: the SRUID of the default revision of the service + """ + service_id = f"{namespace}/{name}" + + for registry in service_registries: + response = _make_service_registry_request(registry, namespace, name) + + if response.status_code == 200: + revision_tag = response.json()["revision_tag"] + + logger.info( + "Found default service revision '%s:%s' in %r registry.", + service_id, + revision_tag, + registry["name"], + ) + + return create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) + + raise octue.exceptions.ServiceNotFound( + f"No revisions for the service {service_id!r} were found in any of the specified service registries: " + f"{service_registries!r}" + ) + + +def raise_if_revision_not_registered(sruid, service_registries): + """Raise an error if the service revision isn't registered in the given service registries. + + :param str sruid: the SRUID of the service revision + :param iter(dict) service_registries: the registries to look for the service revision in + :raise octue.exceptions.ServiceNotFound: if the service revision isn't registered in any of the service registries + :return None: + """ + namespace, name, revision_tag = split_service_id(sruid, require_revision_tag=True) + + for registry in service_registries: + response = _make_service_registry_request(registry, namespace, name, revision_tag) + + if response.status_code == 200: + logger.info("Found service revision %r in %r registry.", sruid, registry["name"]) + return + + raise octue.exceptions.ServiceNotFound( + f"Service revision {sruid!r} was not found in any of the specified service registries: {service_registries!r}" + ) + + +def _make_service_registry_request(registry, namespace, name, revision_tag=None): + """Make an authenticated request to a service registry about a service. + + :param dict registry: a dictionary with the keys "endpoint" and "name" + :param str namespace: the namespace of the service + :param str name: the name of the service + :param str|None revision_tag: the revision tag for a revision of the service + :raise requests.exceptions.HTTPError: if the request fails with a status code other than 404 + :return requests.Response: the response from the service registry + """ + id_token = _get_google_cloud_id_token(registry) + + response = requests.get( + f"{registry['endpoint']}/{namespace}/{name}", + params={"revision_tag": revision_tag}, + headers={"Authorization": f"Bearer {id_token}"}, + ) + + if response.status_code != 404: + response.raise_for_status() + + return response + + +def _get_google_cloud_id_token(registry): + """Get an ID token for Google Cloud. + + :param dict registry: a dictionary with the keys "endpoint" and "name" + :return str: an ID token for Google Cloud + """ + return google.oauth2.id_token.fetch_id_token(google.auth.transport.requests.Request(), registry["endpoint"]) diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index dd041a38a..967940b75 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -1,13 +1,12 @@ import logging import os import re +import uuid import coolname -import requests import octue.exceptions - logger = logging.getLogger(__name__) SERVICE_NAMESPACE_AND_NAME_PATTERN = r"([a-z0-9])+(-([a-z0-9])+)*" @@ -19,6 +18,8 @@ SRUID_PATTERN = rf"^{SERVICE_NAMESPACE_AND_NAME_PATTERN}\/{SERVICE_NAMESPACE_AND_NAME_PATTERN}:{REVISION_TAG_PATTERN}$" COMPILED_SRUID_PATTERN = re.compile(SRUID_PATTERN) +DEFAULT_NAMESPACE = "default" + def get_sruid_parts(service_configuration): """Get the namespace, name, and revision tag for the service from either the service environment variables or the @@ -61,16 +62,16 @@ def get_sruid_parts(service_configuration): return service_namespace, service_name, service_revision_tag -def create_sruid(namespace, name, revision_tag=None): - """Create and validate a service revision unique identifier (SRUID) from a namespace, name, and revision tag. If no - revision tag is given, a "cool name" revision tag is generated. +def create_sruid(namespace=DEFAULT_NAMESPACE, name=None, revision_tag=None): + """Create and validate a service revision unique identifier (SRUID) from a namespace, name, and revision tag. :param str namespace: the name of the group to which the service belongs - :param str name: the name of the service - :param str|None revision_tag: a tag that uniquely identifies a particular revision of the service + :param str|None name: the name of the service; if not provided, a UUID is generated + :param str|None revision_tag: a tag that uniquely identifies a particular revision of the service; if not provided, a "cool name" is generated :raise octue.exceptions.InvalidServiceID: if any of the namespace, name, or revision tag are invalid :return str: the valid SRUID comprising the namespace, name, and revision tag """ + name = name or str(uuid.uuid4()) revision_tag = revision_tag or coolname.generate_slug(2) validate_sruid(namespace=namespace, name=name, revision_tag=revision_tag) return f"{namespace}/{name}:{revision_tag}" @@ -240,52 +241,3 @@ def split_service_id(service_id, require_revision_tag=False): validate_sruid(namespace=namespace, name=name, revision_tag=revision_tag) return namespace, name, revision_tag - - -def get_default_sruid(namespace, name, service_registries): - """Get the SRUID of the default revision of the service `/` if it exists in one of the specified - service registries. The registries should be provided in priority order so that, if more than one registry contains - a matching service, the revision that's returned is taken from the highest priority (first) registry. - - :param str namespace: the namespace of the service - :param str name: the name of the service - :param iter(dict) service_registries: the registries to look for the service in; the registries should be in priority order in case more than one has a service with the given namespace and name - :raise octue.exceptions.ServiceNotFound: if a revision can't be found for the service in the service registries - :return str: the SRUID of the default revision of the service - """ - service_id = f"{namespace}/{name}" - - for registry in service_registries: - response = requests.get(f"{registry['endpoint']}/{service_id}") - - if response.ok: - revision_tag = response.json()["revision_tag"] - logger.info("Found service revision '%s:%s' in %r registry.", service_id, revision_tag, registry["name"]) - return create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) - - raise octue.exceptions.ServiceNotFound( - f"No revisions for the service {service_id!r} were found in any of the specified service registries: " - f"{service_registries!r}" - ) - - -def raise_if_revision_not_registered(sruid, service_registries): - """Raise an error if the service revision isn't registered in the given service registries. - - :param str sruid: the SRUID of the service revision - :param iter(dict) service_registries: the registries to look for the service revision in - :raise octue.exceptions.ServiceNotFound: if the service revision isn't registered in any of the service registries - :return None: - """ - namespace, name, revision_tag = split_service_id(sruid, require_revision_tag=True) - - for registry in service_registries: - response = requests.get(f"{registry['endpoint']}/{namespace}/{name}?revision_tag={revision_tag}") - - if response.ok: - logger.info("Found service revision %r in %r registry.", sruid, registry["name"]) - return - - raise octue.exceptions.ServiceNotFound( - f"Service revision {sruid!r} was not found in any of the specified service registries: {service_registries!r}" - ) diff --git a/octue/cloud/storage/client.py b/octue/cloud/storage/client.py index 956f4802f..c45e5da45 100644 --- a/octue/cloud/storage/client.py +++ b/octue/cloud/storage/client.py @@ -6,21 +6,23 @@ import os import warnings -import google.api_core.exceptions -import google.auth.exceptions from google import auth +import google.api_core.exceptions from google.auth import compute_engine +import google.auth.exceptions from google.auth.transport import requests as google_requests from google.cloud.storage import Client from google.cloud.storage.constants import _DEFAULT_TIMEOUT from google.cloud.storage.retry import DEFAULT_RETRY -from google_crc32c import Checksum from octue.cloud import storage from octue.exceptions import CloudStorageBucketNotFound from octue.utils.decoders import OctueJSONDecoder from octue.utils.encoders import OctueJSONEncoder +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning, module="google_crc32c") + from google_crc32c import Checksum logger = logging.getLogger(__name__) @@ -317,8 +319,8 @@ def generate_signed_url(self, cloud_path, expiration=datetime.timedelta(days=7)) blob = self._blob(cloud_path) try: - # Use compute engine credentials if running on e.g. Google Cloud Run, performing a refresh request to get - # the access token of the credentials (otherwise it's `None`). + # Use compute engine credentials if running in Google Cloud, performing a refresh request to get the access + # token of the credentials (otherwise it's `None`). credentials, _ = google.auth.default() request = google_requests.Request() credentials.refresh(request) diff --git a/octue/compatibility.py b/octue/compatibility.py index abd402470..8d1d9f823 100644 --- a/octue/compatibility.py +++ b/octue/compatibility.py @@ -2,7 +2,6 @@ import logging import os - logger = logging.getLogger(__name__) @@ -36,18 +35,18 @@ def is_compatible(parent_sdk_version, child_sdk_version): return VERSION_COMPATIBILITIES[parent_sdk_version][child_sdk_version] -def warn_if_incompatible(parent_sdk_version, child_sdk_version): - """Log a warning if the parent SDK version isn't compatible with the child SDK version, or if compatibility can't be - checked due to an absence of version information for one of them. +def warn_if_incompatible(sender_sdk_version, recipient_sdk_version): + """Log a warning if the sender's SDK version isn't compatible with the recipient's SDK version, or if compatibility + can't be checked due to an absence of version information for one of them. - :param str|None parent_sdk_version: the version of the Octue SDK running locally / on the local service - :param str|None child_sdk_version: the version of the Octue SDK running on the remote service + :param str|None sender_sdk_version: the version of the Octue SDK running on the sender + :param str|None recipient_sdk_version: the version of the Octue SDK running on the recipient :return None: """ - if not parent_sdk_version: - missing_service_version_information = "parent" - elif not child_sdk_version: - missing_service_version_information = "child" + if not sender_sdk_version: + missing_service_version_information = "sender" + elif not recipient_sdk_version: + missing_service_version_information = "recipient" else: missing_service_version_information = None @@ -59,10 +58,10 @@ def warn_if_incompatible(parent_sdk_version, child_sdk_version): ) return - if not is_compatible(parent_sdk_version, child_sdk_version): + if not is_compatible(sender_sdk_version, recipient_sdk_version): logger.warning( - "The parent's Octue SDK version %s is incompatible with the child's version %s. Please update either or " - "both to the latest version.", - parent_sdk_version, - child_sdk_version, + "The sender's Octue SDK version %s is incompatible with the recipient's version %s. Please update either " + "or both to the latest version.", + sender_sdk_version, + recipient_sdk_version, ) diff --git a/octue/configuration.py b/octue/configuration.py index 150ef3922..50d83f35f 100644 --- a/octue/configuration.py +++ b/octue/configuration.py @@ -4,7 +4,6 @@ import yaml - logger = logging.getLogger(__name__) @@ -76,16 +75,23 @@ def __init__( logger.warning(f"The following keyword arguments were not used by {type(self).__name__}: {kwargs!r}.") @classmethod - def from_file(cls, path=None): + def from_file(cls, path=None, allow_not_found=False): """Load a service configuration from a YAML file. :param str|None path: the path to the service configuration YAML file; if not provided, the `OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` is used - :return ServiceConfiguration: the service configuration loaded from the file + :param bool allow_not_found: if `True`, return `None` instead of raising an error if a service configuration file isn't found + :return ServiceConfiguration|None: the service configuration loaded from the file """ path = path or os.environ.get("OCTUE_SERVICE_CONFIGURATION_PATH", DEFAULT_SERVICE_CONFIGURATION_PATH) - with open(path) as f: - raw_service_configuration = yaml.load(f, Loader=yaml.SafeLoader) + try: + with open(path) as f: + raw_service_configuration = yaml.load(f, Loader=yaml.SafeLoader) + except FileNotFoundError as error: + if allow_not_found: + return None + else: + raise error absolute_path = os.path.abspath(path) logger.info("Service configuration loaded from %r.", absolute_path) diff --git a/octue/definitions.py b/octue/definitions.py index f16992a3d..87b81998f 100644 --- a/octue/definitions.py +++ b/octue/definitions.py @@ -1,3 +1,5 @@ +import importlib.metadata + VALUES_FILENAME = "values.json" MANIFEST_FILENAME = "manifest.json" @@ -16,4 +18,6 @@ # TODO this should probably be defined in twined RUN_STRANDS = ("input_values", "input_manifest", "credentials", "children") -GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION", "GOOGLE_CLOUD_RUN", "GOOGLE_DATAFLOW"} +GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION"} +LOCAL_SDK_VERSION = importlib.metadata.version("octue") +DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL = 360 diff --git a/octue/log_handlers.py b/octue/log_handlers.py index 1a50e1e44..3c0fb2665 100644 --- a/octue/log_handlers.py +++ b/octue/log_handlers.py @@ -5,7 +5,6 @@ from octue.definitions import GOOGLE_COMPUTE_PROVIDERS - if os.environ.get("COMPUTE_PROVIDER", "UNKNOWN") in GOOGLE_COMPUTE_PROVIDERS: # Google Cloud logs don't support colour currently - provide a no-operation function. colourise = lambda string, text_colour=None, background_colour=None: string @@ -189,8 +188,8 @@ def get_remote_handler( def get_log_record_attributes_for_environment(): - """Get the correct log record attributes for the environment. If the environment is Google Cloud Run, get log record - attributes not including the timestamp in the log context to avoid the date appearing twice in the Google Cloud Run + """Get the correct log record attributes for the environment. If the environment is in Google Cloud, get log record + attributes not including the timestamp in the log context to avoid the date appearing twice in the Google Cloud logs (Google adds its own timestamp to log messages). Otherwise, get log record attributes including the timestamp. :return list: diff --git a/octue/metadata/version_compatibilities.json b/octue/metadata/version_compatibilities.json index e10582ca2..55c3c1574 100644 --- a/octue/metadata/version_compatibilities.json +++ b/octue/metadata/version_compatibilities.json @@ -49,7 +49,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.40.1": { "0.40.1": true, @@ -101,7 +102,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.40.2": { "0.41.0": true, @@ -153,7 +155,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.41.0": { "0.41.0": true, @@ -205,7 +208,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.41.1": { "0.41.1": true, @@ -257,7 +261,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.42.0": { "0.42.0": true, @@ -309,7 +314,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.42.1": { "0.43.2": true, @@ -361,7 +367,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.0": { "0.43.2": true, @@ -413,7 +420,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.1": { "0.43.2": true, @@ -465,7 +473,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.2": { "0.43.2": true, @@ -517,7 +526,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.3": { "0.43.3": true, @@ -569,7 +579,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.4": { "0.43.4": true, @@ -621,7 +632,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.5": { "0.43.5": true, @@ -673,7 +685,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.6": { "0.43.6": true, @@ -725,7 +738,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.7": { "0.43.7": true, @@ -777,7 +791,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.44.0": { "0.44.0": true, @@ -829,7 +844,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.45.0": { "0.45.0": true, @@ -881,7 +897,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.0": { "0.46.0": true, @@ -933,7 +950,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.1": { "0.46.1": true, @@ -985,7 +1003,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.2": { "0.46.2": true, @@ -1037,7 +1056,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.3": { "0.46.3": true, @@ -1089,7 +1109,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.47.0": { "0.47.0": true, @@ -1141,7 +1162,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.47.1": { "0.47.1": true, @@ -1193,7 +1215,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.47.2": { "0.47.2": true, @@ -1245,7 +1268,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.48.0": { "0.48.0": true, @@ -1297,7 +1321,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.49.0": { "0.49.1": true, @@ -1349,7 +1374,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.49.1": { "0.49.1": true, @@ -1401,7 +1427,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.49.2": { "0.49.2": true, @@ -1453,7 +1480,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.50.0": { "0.50.0": true, @@ -1505,7 +1533,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.50.1": { "0.51.0": false, @@ -1557,7 +1586,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.51.0": { "0.51.0": true, @@ -1609,7 +1639,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.52.0": { "0.51.0": true, @@ -1661,7 +1692,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.52.1": { "0.51.0": true, @@ -1713,7 +1745,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.52.2": { "0.51.0": true, @@ -1765,7 +1798,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.53.0": { "0.51.0": false, @@ -1817,7 +1851,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.54.0": { "0.51.0": false, @@ -1869,7 +1904,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.55.0": { "0.51.0": false, @@ -1921,7 +1957,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.56.0": { "0.51.0": false, @@ -1973,7 +2010,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.57.0": { "0.51.0": false, @@ -2025,7 +2063,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.57.1": { "0.51.0": false, @@ -2077,7 +2116,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.57.2": { "0.51.0": false, @@ -2129,7 +2169,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.58.0": { "0.51.0": false, @@ -2181,7 +2222,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.59.0": { "0.51.0": false, @@ -2233,7 +2275,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.59.1": { "0.51.0": false, @@ -2285,7 +2328,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.60.0": { "0.51.0": false, @@ -2337,7 +2381,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.60.1": { "0.51.0": false, @@ -2389,7 +2434,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.60.2": { "0.51.0": false, @@ -2441,7 +2487,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.61.0": { "0.51.0": false, @@ -2493,7 +2540,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.61.1": { "0.51.0": false, @@ -2545,7 +2593,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.61.2": { "0.51.0": false, @@ -2597,6 +2646,60 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false + }, + "0.62.0": { + "0.51.0": false, + "0.50.1": false, + "0.50.0": false, + "0.49.2": false, + "0.49.1": false, + "0.49.0": false, + "0.48.0": false, + "0.47.2": false, + "0.47.1": false, + "0.47.0": false, + "0.46.3": false, + "0.46.2": false, + "0.46.1": false, + "0.46.0": false, + "0.45.0": false, + "0.44.0": false, + "0.43.7": false, + "0.43.6": false, + "0.43.5": false, + "0.43.4": false, + "0.43.3": false, + "0.43.2": false, + "0.43.1": false, + "0.43.0": false, + "0.42.1": false, + "0.42.0": false, + "0.41.1": false, + "0.41.0": false, + "0.40.2": false, + "0.40.1": false, + "0.40.0": false, + "0.52.0": false, + "0.52.1": false, + "0.52.2": false, + "0.53.0": false, + "0.54.0": false, + "0.55.0": false, + "0.56.0": false, + "0.57.0": false, + "0.57.1": false, + "0.57.2": false, + "0.58.0": false, + "0.59.0": false, + "0.59.1": false, + "0.60.0": false, + "0.60.1": false, + "0.60.2": false, + "0.61.0": false, + "0.61.1": false, + "0.61.2": false, + "0.62.0": true } } diff --git a/octue/mixins/hashable.py b/octue/mixins/hashable.py index d2689fea6..10ff7fc23 100644 --- a/octue/mixins/hashable.py +++ b/octue/mixins/hashable.py @@ -1,8 +1,11 @@ import base64 import collections.abc import datetime +import warnings -from google_crc32c import Checksum +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning, module="google_crc32c") + from google_crc32c import Checksum EMPTY_STRING_HASH_VALUE = "AAAAAA==" diff --git a/octue/mixins/metadata.py b/octue/mixins/metadata.py index 721613c4d..5adaf034d 100644 --- a/octue/mixins/metadata.py +++ b/octue/mixins/metadata.py @@ -1,6 +1,6 @@ -import importlib.metadata from abc import abstractmethod +from octue.definitions import LOCAL_SDK_VERSION from octue.mixins.hashable import Hashable @@ -36,7 +36,7 @@ def metadata(self, include_id=True, include_sdk_version=True, **kwargs): metadata = {name: getattr(self, name) for name in self._METADATA_ATTRIBUTES} if include_sdk_version: - metadata["sdk_version"] = importlib.metadata.version("octue") + metadata["sdk_version"] = LOCAL_SDK_VERSION if not include_id and "id" in metadata: del metadata["id"] diff --git a/octue/resources/child.py b/octue/resources/child.py index c06917e40..53bcea6bc 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -4,9 +4,9 @@ import os from octue.cloud.pub_sub.service import Service +from octue.definitions import DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL from octue.resources import service_backends - logger = logging.getLogger(__name__) BACKEND_TO_SERVICE_MAPPING = {"GCPPubSubBackend": Service} @@ -69,12 +69,15 @@ def ask( push_endpoint=None, asynchronous=False, retry_count=0, + cpus=None, + memory=None, + ephemeral_storage=None, raise_errors=True, max_retries=0, prevent_retries_when=None, log_errors=True, timeout=86400, - maximum_heartbeat_interval=300, + maximum_heartbeat_interval=DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL, ): """Ask the child either: - A synchronous (ask-and-wait) question and wait for it to return an output. Questions are synchronous if @@ -97,11 +100,14 @@ def ask( :param str|None push_endpoint: if answers to the question should be pushed to an endpoint, provide its URL here (the returned subscription will be a push subscription); if not, leave this as `None` :param bool asynchronous: if `True`, don't wait for an answer or create an answer subscription (the result and other events can be retrieved from the event store later) :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param int|None cpus: the number of CPUs to request for the question; defaults to the number set by the child service + :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service + :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service :param bool raise_errors: if `True` and the question fails, raise the error; if False, return the error in place of the answer :param int max_retries: if `raise_errors=False` and the question fails, retry the question up to this number of times :param list(type)|None prevent_retries_when: if `raise_errors=False` and the question fails, prevent retrying the question if it fails with an exception type in this list :param bool log_errors: if `True`, `raise_errors=False`, and the question fails after its final retry, log the error - :param float timeout: time in seconds to wait for an answer before raising a timeout error + :param float timeout: time to wait for an answer before raising a timeout error [s] :param float|int maximum_heartbeat_interval: the maximum amount of time (in seconds) allowed between child heartbeats before an error is raised :raise TimeoutError: if the timeout is exceeded while waiting for an answer :raise Exception: if the question raises an error and `raise_errors=True` @@ -123,6 +129,9 @@ def ask( "push_endpoint": push_endpoint, "asynchronous": asynchronous, "retry_count": retry_count, + "cpus": cpus, + "memory": memory, + "ephemeral_storage": ephemeral_storage, "timeout": timeout, } @@ -131,6 +140,8 @@ def ask( if push_endpoint or asynchronous: return subscription, question_uuid + logger.info("Waiting for question to be accepted...") + try: answer = self._service.wait_for_answer( subscription=subscription, @@ -144,7 +155,7 @@ def ask( except Exception as e: logger.error( - "Question %r failed. Run 'octue get-diagnostics gs:///%s " + "Question %r failed. Run 'octue question diagnostics gs:///%s " "--download-datasets' to get the crash diagnostics.", question_uuid, question_uuid, @@ -226,3 +237,13 @@ def ask_multiple( # Convert dictionary to list in asking order. return [answer[1] for answer in sorted(answers.items(), key=lambda item: item[0])] + + # def cancel(self, question_uuid, event_store_table_id, timeout=30): + # """Request cancellation of a running question. + # + # :param str question_uuid: the question UUID of the question to cancel + # :param str event_store_table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-project.your-dataset.your-table" + # :param float timeout: time to wait for the cancellation to send before raising a timeout error [s] + # :return None: + # """ + # self._service.cancel(question_uuid=question_uuid, event_store_table_id=event_store_table_id, timeout=timeout) diff --git a/octue/resources/datafile.py b/octue/resources/datafile.py index 1d89e8dff..bb02153e7 100644 --- a/octue/resources/datafile.py +++ b/octue/resources/datafile.py @@ -7,10 +7,9 @@ import os import shutil import tempfile +import warnings import requests -from google_crc32c import Checksum - # The `h5py` package is only needed if dealing with HDF5 files. It's only available if the `hdf5` extra is provided # during installation of `octue`. @@ -27,6 +26,10 @@ from octue.utils.decoders import OctueJSONDecoder from octue.utils.metadata import METADATA_FILENAME, UpdateLocalMetadata, load_local_metadata_file +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning, module="google_crc32c") + from google_crc32c import Checksum + logger = logging.getLogger(__name__) diff --git a/octue/utils/testing.py b/octue/utils/testing.py index 8af330a2a..75f57d456 100644 --- a/octue/utils/testing.py +++ b/octue/utils/testing.py @@ -7,7 +7,7 @@ def load_test_fixture_from_diagnostics(path): - """Load a test fixture from service diagnostics downloaded using the `octue get-diagnostics` CLI + """Load a test fixture from service diagnostics downloaded using the `octue question diagnostics` CLI command. The configuration values, configuration manifest, input values, and input manifest are returned if available. A tuple of child emulators is returned if the service has children and asked questions to any of them. Each child emulator corresponds to one question asked to a child. The child emulators are in the same order as the diff --git a/poetry.lock b/poetry.lock index a7de3f187..c519c5a5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,15 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.16" +description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] @@ -17,6 +18,7 @@ version = "1.4.1" description = "Handy tools for working with URLs and APIs." optional = false python-versions = ">=3.6.1" +groups = ["dev"] files = [ {file = "apeye-1.4.1-py3-none-any.whl", hash = "sha256:44e58a9104ec189bf42e76b3a7fe91e2b2879d96d48e9a77e5e32ff699c9204e"}, {file = "apeye-1.4.1.tar.gz", hash = "sha256:14ea542fad689e3bfdbda2189a354a4908e90aee4bf84c15ab75d68453d76a36"}, @@ -38,6 +40,7 @@ version = "1.1.5" description = "Core (offline) functionality for the apeye library." optional = false python-versions = ">=3.6.1" +groups = ["dev"] files = [ {file = "apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf"}, {file = "apeye_core-1.1.5.tar.gz", hash = "sha256:5de72ed3d00cc9b20fea55e54b7ab8f5ef8500eb33a5368bc162a5585e238a55"}, @@ -53,6 +56,7 @@ version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -64,18 +68,19 @@ version = "24.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "autodocsumm" @@ -83,6 +88,7 @@ version = "0.2.14" description = "Extended sphinx autodoc including automatic autosummaries" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0"}, {file = "autodocsumm-0.2.14.tar.gz", hash = "sha256:2839a9d4facc3c4eccd306c08695540911042b46eeafcdc3203e6d0bab40bc77"}, @@ -97,51 +103,22 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "beautifulsoup4" version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" +groups = ["dev"] files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, @@ -157,26 +134,16 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "blinker" -version = "1.8.2" -description = "Fast, simple object-to-object and broadcast signaling" -optional = false -python-versions = ">=3.8" -files = [ - {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, - {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, -] - [[package]] name = "cachecontrol" -version = "0.14.1" +version = "0.14.2" description = "httplib2 caching for requests" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, - {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, + {file = "cachecontrol-0.14.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, + {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, ] [package.dependencies] @@ -195,6 +162,7 @@ version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, @@ -206,6 +174,7 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -217,6 +186,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -224,127 +194,116 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -356,10 +315,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} [[package]] name = "coolname" @@ -367,6 +328,7 @@ version = "2.2.0" description = "Random name and slug generator" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "coolname-2.2.0-py2.py3-none-any.whl", hash = "sha256:4d1563186cfaf71b394d5df4c744f8c41303b6846413645e31d31915cdeb13e8"}, {file = "coolname-2.2.0.tar.gz", hash = "sha256:6c5d5731759104479e7ca195a9b64f7900ac5bead40183c09323c7d0be9e75c7"}, @@ -378,6 +340,7 @@ version = "5.5" description = "Code coverage measurement for Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +groups = ["dev"] files = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, @@ -442,6 +405,7 @@ version = "2.11.1" description = "A CSS Cascading Style Sheets library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1"}, {file = "cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2"}, @@ -452,7 +416,7 @@ more-itertools = "*" [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["cssselect", "importlib-resources", "jaraco.test (>=5.1)", "lxml", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +test = ["cssselect", "importlib-resources ; python_version < \"3.9\"", "jaraco.test (>=5.1)", "lxml ; python_version < \"3.11\"", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "dateparser" @@ -460,6 +424,7 @@ version = "1.1.1" description = "Date parsing library designed to parse dates from HTML pages" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "dateparser-1.1.1-py2.py3-none-any.whl", hash = "sha256:9600874312ff28a41f96ec7ccdc73be1d1c44435719da47fea3339d55ff5a628"}, {file = "dateparser-1.1.1.tar.gz", hash = "sha256:038196b1f12c7397e38aad3d61588833257f6f552baa63a1499e6987fa8d42d9"}, @@ -472,7 +437,7 @@ regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27,<2022.3.15" tzlocal = "*" [package.extras] -calendars = ["convertdate", "convertdate", "hijri-converter"] +calendars = ["convertdate ; python_version < \"3.6\"", "convertdate ; python_version >= \"3.6\"", "hijri-converter ; python_version >= \"3.6\""] fasttext = ["fasttext"] langdetect = ["langdetect"] @@ -482,6 +447,7 @@ version = "1.2.15" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] files = [ {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, @@ -491,7 +457,7 @@ files = [ wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools ; python_version >= \"3.12\"", "sphinx (<2)", "tox"] [[package]] name = "dict2css" @@ -499,6 +465,7 @@ version = "0.3.0.post1" description = "A μ-library for constructing cascading style sheets from Python dictionaries." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "dict2css-0.3.0.post1-py3-none-any.whl", hash = "sha256:f006a6b774c3e31869015122ae82c491fd25e7de4a75607a62aa3e798f837e0d"}, {file = "dict2css-0.3.0.post1.tar.gz", hash = "sha256:89c544c21c4ca7472c3fffb9d37d3d926f606329afdb751dc1de67a411b70719"}, @@ -514,6 +481,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -525,6 +493,7 @@ version = "0.18.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] files = [ {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, @@ -536,14 +505,13 @@ version = "3.9.0" description = "Helpful functions for Python 🐍 🛠️" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "domdf_python_tools-3.9.0-py3-none-any.whl", hash = "sha256:4e1ef365cbc24627d6d1e90cf7d46d8ab8df967e1237f4a26885f6986c78872e"}, {file = "domdf_python_tools-3.9.0.tar.gz", hash = "sha256:1f8a96971178333a55e083e35610d7688cd7620ad2b99790164e1fc1a3614c18"}, ] [package.dependencies] -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.9\""} -importlib-resources = {version = ">=3.0.0", markers = "python_version < \"3.9\""} natsort = ">=7.0.1" typing-extensions = ">=3.7.4.1" @@ -557,6 +525,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -571,6 +541,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -579,30 +550,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] - -[[package]] -name = "flask" -version = "2.3.3" -description = "A simple framework for building complex web applications." -optional = false -python-versions = ">=3.8" -files = [ - {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, - {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, -] - -[package.dependencies] -blinker = ">=1.6.2" -click = ">=8.1.3" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.1.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=2.3.7" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "fs" @@ -610,6 +558,7 @@ version = "2.4.16" description = "Python's filesystem abstraction layer" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "fs-2.4.16-py2.py3-none-any.whl", hash = "sha256:660064febbccda264ae0b6bace80a8d1be9e089e0a5eb2427b7d517f9a91545c"}, {file = "fs-2.4.16.tar.gz", hash = "sha256:ae97c7d51213f4b70b6a958292530289090de3a7e15841e108fbe144f069d313"}, @@ -621,7 +570,7 @@ setuptools = "*" six = ">=1.10,<2.0" [package.extras] -scandir = ["scandir (>=1.5,<2.0)"] +scandir = ["scandir (>=1.5,<2.0) ; python_version < \"3.5\""] [[package]] name = "gcp-storage-emulator" @@ -629,6 +578,7 @@ version = "2022.6.11" description = "A stub emulator for the Google Cloud Storage API" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "gcp-storage-emulator-2022.6.11.tar.gz", hash = "sha256:fbc896723df95b7527a4ced499bfdd374247dfc40d6992bfa34da67e66bad4be"}, {file = "gcp_storage_emulator-2022.6.11-py3-none-any.whl", hash = "sha256:81271737f099cec22b3b4d0ceca5d43f484bfc39770a7d98e6545eb94bf46a1b"}, @@ -644,6 +594,7 @@ version = "2.24.0" description = "Google API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_api_core-2.24.0-py3-none-any.whl", hash = "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9"}, {file = "google_api_core-2.24.0.tar.gz", hash = "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"}, @@ -653,23 +604,23 @@ files = [ google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" grpcio = [ + {version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ + {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] proto-plus = [ + {version = ">=1.22.3,<2.0.0dev"}, {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -679,6 +630,7 @@ version = "2.37.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"}, {file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"}, @@ -703,6 +655,7 @@ version = "3.27.0" description = "Google BigQuery API client library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_cloud_bigquery-3.27.0-py2.py3-none-any.whl", hash = "sha256:b53b0431e5ba362976a4cd8acce72194b4116cdf8115030c7b339b884603fcc3"}, {file = "google_cloud_bigquery-3.27.0.tar.gz", hash = "sha256:379c524054d7b090fa56d0c22662cc6e6458a6229b6754c0e7177e3a73421d2c"}, @@ -718,14 +671,14 @@ python-dateutil = ">=2.7.3,<3.0dev" requests = ">=2.21.0,<3.0.0dev" [package.extras] -all = ["Shapely (>=1.8.4,<3.0.0dev)", "bigquery-magics (>=0.1.0)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "importlib-metadata (>=1.0.0)", "ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.22.3,<2.0.0dev)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] +all = ["Shapely (>=1.8.4,<3.0.0dev)", "bigquery-magics (>=0.1.0)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "importlib-metadata (>=1.0.0) ; python_version < \"3.8\"", "ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.22.3,<2.0.0dev)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] bigquery-v2 = ["proto-plus (>=1.22.3,<2.0.0dev)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev)"] -bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "pyarrow (>=3.0.0)"] +bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "pyarrow (>=3.0.0)"] geopandas = ["Shapely (>=1.8.4,<3.0.0dev)", "geopandas (>=0.9.0,<1.0dev)"] ipython = ["bigquery-magics (>=0.1.0)"] ipywidgets = ["ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)"] opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)"] -pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] +pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0) ; python_version < \"3.8\"", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] [[package]] @@ -734,6 +687,7 @@ version = "2.4.1" description = "Google Cloud API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, @@ -748,13 +702,14 @@ grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-pubsub" -version = "2.27.1" +version = "2.27.2" description = "Google Cloud Pub/Sub API client library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "google_cloud_pubsub-2.27.1-py2.py3-none-any.whl", hash = "sha256:3ca8980c198a847ee464845ab60f05478d4819cf693c9950ee89da96f0b80a41"}, - {file = "google_cloud_pubsub-2.27.1.tar.gz", hash = "sha256:7119dbc5af4b915ecdfa1289919f791a432927eaaa7bbfbeb740e6d7020c181e"}, + {file = "google_cloud_pubsub-2.27.2-py2.py3-none-any.whl", hash = "sha256:a919f84fdea683b0a02464e38dd32332edbcbc8e85da82070079a57791119fd6"}, + {file = "google_cloud_pubsub-2.27.2.tar.gz", hash = "sha256:d92c156c7ddd0e5125008f977898198d7b1ae766026056497271bec4909647fe"}, ] [package.dependencies] @@ -766,9 +721,9 @@ grpcio-status = ">=1.33.2" opentelemetry-api = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} opentelemetry-sdk = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} proto-plus = [ + {version = ">=1.22.0,<2.0.0dev"}, + {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}, {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\" and python_version < \"3.13\""}, - {version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" @@ -781,6 +736,7 @@ version = "2.22.0" description = "Google Cloud Secret Manager API client library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_cloud_secret_manager-2.22.0-py2.py3-none-any.whl", hash = "sha256:9e23a8165ed718de56543723b1e21c394f2cee9ababddcac8ceecc9f427d2696"}, {file = "google_cloud_secret_manager-2.22.0.tar.gz", hash = "sha256:5dd95ac6243687f86fd803316c0768f507028958b8a2e69b3aa0ace7ac654bf4"}, @@ -791,8 +747,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extr google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" proto-plus = [ + {version = ">=1.22.3,<2.0.0dev"}, {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" @@ -802,6 +758,7 @@ version = "2.19.0" description = "Google Cloud Storage API client library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba"}, {file = "google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2"}, @@ -825,6 +782,7 @@ version = "1.3.0" description = "A python wrapper of the C library 'Google CRC32C'" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "google-crc32c-1.3.0.tar.gz", hash = "sha256:276de6273eb074a35bc598f8efbc00c7869c5cf2e29c90748fccc8c898c244df"}, {file = "google_crc32c-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cb6994fff247987c66a8a4e550ef374671c2b82e3c0d2115e689d21e511a652d"}, @@ -880,6 +838,7 @@ version = "2.7.2" description = "Utilities for Google Media Downloads and Resumable Uploads" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa"}, {file = "google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0"}, @@ -898,6 +857,7 @@ version = "1.66.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, @@ -912,13 +872,14 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "grpc-google-iam-v1" -version = "0.13.1" +version = "0.14.0" description = "IAM API client library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001"}, - {file = "grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e"}, + {file = "grpc_google_iam_v1-0.14.0-py2.py3-none-any.whl", hash = "sha256:fb4a084b30099ba3ab07d61d620a0d4429570b13ff53bd37bac75235f98b7da4"}, + {file = "grpc_google_iam_v1-0.14.0.tar.gz", hash = "sha256:c66e07aa642e39bb37950f9e7f491f70dad150ac9801263b42b2814307c2df99"}, ] [package.dependencies] @@ -928,140 +889,128 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4 [[package]] name = "grpcio" -version = "1.68.1" +version = "1.69.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" -files = [ - {file = "grpcio-1.68.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:d35740e3f45f60f3c37b1e6f2f4702c23867b9ce21c6410254c9c682237da68d"}, - {file = "grpcio-1.68.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:d99abcd61760ebb34bdff37e5a3ba333c5cc09feda8c1ad42547bea0416ada78"}, - {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f8261fa2a5f679abeb2a0a93ad056d765cdca1c47745eda3f2d87f874ff4b8c9"}, - {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0feb02205a27caca128627bd1df4ee7212db051019a9afa76f4bb6a1a80ca95e"}, - {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:919d7f18f63bcad3a0f81146188e90274fde800a94e35d42ffe9eadf6a9a6330"}, - {file = "grpcio-1.68.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:963cc8d7d79b12c56008aabd8b457f400952dbea8997dd185f155e2f228db079"}, - {file = "grpcio-1.68.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ccf2ebd2de2d6661e2520dae293298a3803a98ebfc099275f113ce1f6c2a80f1"}, - {file = "grpcio-1.68.1-cp310-cp310-win32.whl", hash = "sha256:2cc1fd04af8399971bcd4f43bd98c22d01029ea2e56e69c34daf2bf8470e47f5"}, - {file = "grpcio-1.68.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2e743e51cb964b4975de572aa8fb95b633f496f9fcb5e257893df3be854746"}, - {file = "grpcio-1.68.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:55857c71641064f01ff0541a1776bfe04a59db5558e82897d35a7793e525774c"}, - {file = "grpcio-1.68.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4b177f5547f1b995826ef529d2eef89cca2f830dd8b2c99ffd5fde4da734ba73"}, - {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:3522c77d7e6606d6665ec8d50e867f13f946a4e00c7df46768f1c85089eae515"}, - {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d1fae6bbf0816415b81db1e82fb3bf56f7857273c84dcbe68cbe046e58e1ccd"}, - {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298ee7f80e26f9483f0b6f94cc0a046caf54400a11b644713bb5b3d8eb387600"}, - {file = "grpcio-1.68.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cbb5780e2e740b6b4f2d208e90453591036ff80c02cc605fea1af8e6fc6b1bbe"}, - {file = "grpcio-1.68.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ddda1aa22495d8acd9dfbafff2866438d12faec4d024ebc2e656784d96328ad0"}, - {file = "grpcio-1.68.1-cp311-cp311-win32.whl", hash = "sha256:b33bd114fa5a83f03ec6b7b262ef9f5cac549d4126f1dc702078767b10c46ed9"}, - {file = "grpcio-1.68.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f20ebec257af55694d8f993e162ddf0d36bd82d4e57f74b31c67b3c6d63d8b2"}, - {file = "grpcio-1.68.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8829924fffb25386995a31998ccbbeaa7367223e647e0122043dfc485a87c666"}, - {file = "grpcio-1.68.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3aed6544e4d523cd6b3119b0916cef3d15ef2da51e088211e4d1eb91a6c7f4f1"}, - {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:4efac5481c696d5cb124ff1c119a78bddbfdd13fc499e3bc0ca81e95fc573684"}, - {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab2d912ca39c51f46baf2a0d92aa265aa96b2443266fc50d234fa88bf877d8e"}, - {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c87ce2a97434dffe7327a4071839ab8e8bffd0054cc74cbe971fba98aedd60"}, - {file = "grpcio-1.68.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e4842e4872ae4ae0f5497bf60a0498fa778c192cc7a9e87877abd2814aca9475"}, - {file = "grpcio-1.68.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:255b1635b0ed81e9f91da4fcc8d43b7ea5520090b9a9ad9340d147066d1d3613"}, - {file = "grpcio-1.68.1-cp312-cp312-win32.whl", hash = "sha256:7dfc914cc31c906297b30463dde0b9be48e36939575eaf2a0a22a8096e69afe5"}, - {file = "grpcio-1.68.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0c8ddabef9c8f41617f213e527254c41e8b96ea9d387c632af878d05db9229c"}, - {file = "grpcio-1.68.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:a47faedc9ea2e7a3b6569795c040aae5895a19dde0c728a48d3c5d7995fda385"}, - {file = "grpcio-1.68.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:390eee4225a661c5cd133c09f5da1ee3c84498dc265fd292a6912b65c421c78c"}, - {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:66a24f3d45c33550703f0abb8b656515b0ab777970fa275693a2f6dc8e35f1c1"}, - {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08079b4934b0bf0a8847f42c197b1d12cba6495a3d43febd7e99ecd1cdc8d54"}, - {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8720c25cd9ac25dd04ee02b69256d0ce35bf8a0f29e20577427355272230965a"}, - {file = "grpcio-1.68.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:04cfd68bf4f38f5bb959ee2361a7546916bd9a50f78617a346b3aeb2b42e2161"}, - {file = "grpcio-1.68.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c28848761a6520c5c6071d2904a18d339a796ebe6b800adc8b3f474c5ce3c3ad"}, - {file = "grpcio-1.68.1-cp313-cp313-win32.whl", hash = "sha256:77d65165fc35cff6e954e7fd4229e05ec76102d4406d4576528d3a3635fc6172"}, - {file = "grpcio-1.68.1-cp313-cp313-win_amd64.whl", hash = "sha256:a8040f85dcb9830d8bbb033ae66d272614cec6faceee88d37a88a9bd1a7a704e"}, - {file = "grpcio-1.68.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:eeb38ff04ab6e5756a2aef6ad8d94e89bb4a51ef96e20f45c44ba190fa0bcaad"}, - {file = "grpcio-1.68.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a3869a6661ec8f81d93f4597da50336718bde9eb13267a699ac7e0a1d6d0bea"}, - {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:2c4cec6177bf325eb6faa6bd834d2ff6aa8bb3b29012cceb4937b86f8b74323c"}, - {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12941d533f3cd45d46f202e3667be8ebf6bcb3573629c7ec12c3e211d99cfccf"}, - {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80af6f1e69c5e68a2be529990684abdd31ed6622e988bf18850075c81bb1ad6e"}, - {file = "grpcio-1.68.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e8dbe3e00771bfe3d04feed8210fc6617006d06d9a2679b74605b9fed3e8362c"}, - {file = "grpcio-1.68.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:83bbf5807dc3ee94ce1de2dfe8a356e1d74101e4b9d7aa8c720cc4818a34aded"}, - {file = "grpcio-1.68.1-cp38-cp38-win32.whl", hash = "sha256:8cb620037a2fd9eeee97b4531880e439ebfcd6d7d78f2e7dcc3726428ab5ef63"}, - {file = "grpcio-1.68.1-cp38-cp38-win_amd64.whl", hash = "sha256:52fbf85aa71263380d330f4fce9f013c0798242e31ede05fcee7fbe40ccfc20d"}, - {file = "grpcio-1.68.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:cb400138e73969eb5e0535d1d06cae6a6f7a15f2cc74add320e2130b8179211a"}, - {file = "grpcio-1.68.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a1b988b40f2fd9de5c820f3a701a43339d8dcf2cb2f1ca137e2c02671cc83ac1"}, - {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:96f473cdacfdd506008a5d7579c9f6a7ff245a9ade92c3c0265eb76cc591914f"}, - {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:37ea3be171f3cf3e7b7e412a98b77685eba9d4fd67421f4a34686a63a65d99f9"}, - {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ceb56c4285754e33bb3c2fa777d055e96e6932351a3082ce3559be47f8024f0"}, - {file = "grpcio-1.68.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dffd29a2961f3263a16d73945b57cd44a8fd0b235740cb14056f0612329b345e"}, - {file = "grpcio-1.68.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:025f790c056815b3bf53da850dd70ebb849fd755a4b1ac822cb65cd631e37d43"}, - {file = "grpcio-1.68.1-cp39-cp39-win32.whl", hash = "sha256:1098f03dedc3b9810810568060dea4ac0822b4062f537b0f53aa015269be0a76"}, - {file = "grpcio-1.68.1-cp39-cp39-win_amd64.whl", hash = "sha256:334ab917792904245a028f10e803fcd5b6f36a7b2173a820c0b5b076555825e1"}, - {file = "grpcio-1.68.1.tar.gz", hash = "sha256:44a8502dd5de653ae6a73e2de50a401d84184f0331d0ac3daeb044e66d5c5054"}, +groups = ["main"] +files = [ + {file = "grpcio-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2060ca95a8db295ae828d0fc1c7f38fb26ccd5edf9aa51a0f44251f5da332e97"}, + {file = "grpcio-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2e52e107261fd8fa8fa457fe44bfadb904ae869d87c1280bf60f93ecd3e79278"}, + {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:316463c0832d5fcdb5e35ff2826d9aa3f26758d29cdfb59a368c1d6c39615a11"}, + {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c9a9c4ac917efab4704b18eed9082ed3b6ad19595f047e8173b5182fec0d5e"}, + {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b3646ced2eae3a0599658eeccc5ba7f303bf51b82514c50715bdd2b109e5ec"}, + {file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3b75aea7c6cb91b341c85e7c1d9db1e09e1dd630b0717f836be94971e015031e"}, + {file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5cfd14175f9db33d4b74d63de87c64bb0ee29ce475ce3c00c01ad2a3dc2a9e51"}, + {file = "grpcio-1.69.0-cp310-cp310-win32.whl", hash = "sha256:9031069d36cb949205293cf0e243abd5e64d6c93e01b078c37921493a41b72dc"}, + {file = "grpcio-1.69.0-cp310-cp310-win_amd64.whl", hash = "sha256:cc89b6c29f3dccbe12d7a3b3f1b3999db4882ae076c1c1f6df231d55dbd767a5"}, + {file = "grpcio-1.69.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8de1b192c29b8ce45ee26a700044717bcbbd21c697fa1124d440548964328561"}, + {file = "grpcio-1.69.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:7e76accf38808f5c5c752b0ab3fd919eb14ff8fafb8db520ad1cc12afff74de6"}, + {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:d5658c3c2660417d82db51e168b277e0ff036d0b0f859fa7576c0ffd2aec1442"}, + {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5494d0e52bf77a2f7eb17c6da662886ca0a731e56c1c85b93505bece8dc6cf4c"}, + {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ed866f9edb574fd9be71bf64c954ce1b88fc93b2a4cbf94af221e9426eb14d6"}, + {file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c5ba38aeac7a2fe353615c6b4213d1fbb3a3c34f86b4aaa8be08baaaee8cc56d"}, + {file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f79e05f5bbf551c4057c227d1b041ace0e78462ac8128e2ad39ec58a382536d2"}, + {file = "grpcio-1.69.0-cp311-cp311-win32.whl", hash = "sha256:bf1f8be0da3fcdb2c1e9f374f3c2d043d606d69f425cd685110dd6d0d2d61258"}, + {file = "grpcio-1.69.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb9302afc3a0e4ba0b225cd651ef8e478bf0070cf11a529175caecd5ea2474e7"}, + {file = "grpcio-1.69.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fc18a4de8c33491ad6f70022af5c460b39611e39578a4d84de0fe92f12d5d47b"}, + {file = "grpcio-1.69.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:0f0270bd9ffbff6961fe1da487bdcd594407ad390cc7960e738725d4807b18c4"}, + {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc48f99cc05e0698e689b51a05933253c69a8c8559a47f605cff83801b03af0e"}, + {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e925954b18d41aeb5ae250262116d0970893b38232689c4240024e4333ac084"}, + {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d222569273720366f68a99cb62e6194681eb763ee1d3b1005840678d4884f9"}, + {file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b62b0f41e6e01a3e5082000b612064c87c93a49b05f7602fe1b7aa9fd5171a1d"}, + {file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:db6f9fd2578dbe37db4b2994c94a1d9c93552ed77dca80e1657bb8a05b898b55"}, + {file = "grpcio-1.69.0-cp312-cp312-win32.whl", hash = "sha256:b192b81076073ed46f4b4dd612b8897d9a1e39d4eabd822e5da7b38497ed77e1"}, + {file = "grpcio-1.69.0-cp312-cp312-win_amd64.whl", hash = "sha256:1227ff7836f7b3a4ab04e5754f1d001fa52a730685d3dc894ed8bc262cc96c01"}, + {file = "grpcio-1.69.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:a78a06911d4081a24a1761d16215a08e9b6d4d29cdbb7e427e6c7e17b06bcc5d"}, + {file = "grpcio-1.69.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:dc5a351927d605b2721cbb46158e431dd49ce66ffbacb03e709dc07a491dde35"}, + {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:3629d8a8185f5139869a6a17865d03113a260e311e78fbe313f1a71603617589"}, + {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9a281878feeb9ae26db0622a19add03922a028d4db684658f16d546601a4870"}, + {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc614e895177ab7e4b70f154d1a7c97e152577ea101d76026d132b7aaba003b"}, + {file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1ee76cd7e2e49cf9264f6812d8c9ac1b85dda0eaea063af07292400f9191750e"}, + {file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0470fa911c503af59ec8bc4c82b371ee4303ececbbdc055f55ce48e38b20fd67"}, + {file = "grpcio-1.69.0-cp313-cp313-win32.whl", hash = "sha256:b650f34aceac8b2d08a4c8d7dc3e8a593f4d9e26d86751ebf74ebf5107d927de"}, + {file = "grpcio-1.69.0-cp313-cp313-win_amd64.whl", hash = "sha256:028337786f11fecb5d7b7fa660475a06aabf7e5e52b5ac2df47414878c0ce7ea"}, + {file = "grpcio-1.69.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:b7f693db593d6bf285e015d5538bf1c86cf9c60ed30b6f7da04a00ed052fe2f3"}, + {file = "grpcio-1.69.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:8b94e83f66dbf6fd642415faca0608590bc5e8d30e2c012b31d7d1b91b1de2fd"}, + {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:b634851b92c090763dde61df0868c730376cdb73a91bcc821af56ae043b09596"}, + {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf5f680d3ed08c15330d7830d06bc65f58ca40c9999309517fd62880d70cb06e"}, + {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:200e48a6e7b00f804cf00a1c26292a5baa96507c7749e70a3ec10ca1a288936e"}, + {file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:45a4704339b6e5b24b0e136dea9ad3815a94f30eb4f1e1d44c4ac484ef11d8dd"}, + {file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85d347cb8237751b23539981dbd2d9d8f6e9ff90082b427b13022b948eb6347a"}, + {file = "grpcio-1.69.0-cp38-cp38-win32.whl", hash = "sha256:60e5de105dc02832dc8f120056306d0ef80932bcf1c0e2b4ca3b676de6dc6505"}, + {file = "grpcio-1.69.0-cp38-cp38-win_amd64.whl", hash = "sha256:282f47d0928e40f25d007f24eb8fa051cb22551e3c74b8248bc9f9bea9c35fe0"}, + {file = "grpcio-1.69.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:dd034d68a2905464c49479b0c209c773737a4245d616234c79c975c7c90eca03"}, + {file = "grpcio-1.69.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:01f834732c22a130bdf3dc154d1053bdbc887eb3ccb7f3e6285cfbfc33d9d5cc"}, + {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:a7f4ed0dcf202a70fe661329f8874bc3775c14bb3911d020d07c82c766ce0eb1"}, + {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd7ea241b10bc5f0bb0f82c0d7896822b7ed122b3ab35c9851b440c1ccf81588"}, + {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f03dc9b4da4c0dc8a1db7a5420f575251d7319b7a839004d8916257ddbe4816"}, + {file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca71d73a270dff052fe4edf74fef142d6ddd1f84175d9ac4a14b7280572ac519"}, + {file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ccbed100dc43704e94ccff9e07680b540d64e4cc89213ab2832b51b4f68a520"}, + {file = "grpcio-1.69.0-cp39-cp39-win32.whl", hash = "sha256:1514341def9c6ec4b7f0b9628be95f620f9d4b99331b7ef0a1845fd33d9b579c"}, + {file = "grpcio-1.69.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1fea55d26d647346acb0069b08dca70984101f2dc95066e003019207212e303"}, + {file = "grpcio-1.69.0.tar.gz", hash = "sha256:936fa44241b5379c5afc344e1260d467bee495747eaf478de825bab2791da6f5"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.68.1)"] +protobuf = ["grpcio-tools (>=1.69.0)"] [[package]] name = "grpcio-status" -version = "1.68.1" +version = "1.69.0" description = "Status proto mapping for gRPC" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "grpcio_status-1.68.1-py3-none-any.whl", hash = "sha256:66f3d8847f665acfd56221333d66f7ad8927903d87242a482996bdb45e8d28fd"}, - {file = "grpcio_status-1.68.1.tar.gz", hash = "sha256:e1378d036c81a1610d7b4c7a146cd663dd13fcc915cf4d7d053929dba5bbb6e1"}, + {file = "grpcio_status-1.69.0-py3-none-any.whl", hash = "sha256:d6b2a3c9562c03a817c628d7ba9a925e209c228762d6d7677ae5c9401a542853"}, + {file = "grpcio_status-1.69.0.tar.gz", hash = "sha256:595ef84e5178d6281caa732ccf68ff83259241608d26b0e9c40a5e66eee2a2d2"}, ] [package.dependencies] googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.68.1" +grpcio = ">=1.69.0" protobuf = ">=5.26.1,<6.0dev" -[[package]] -name = "gunicorn" -version = "22.0.0" -description = "WSGI HTTP Server for UNIX" -optional = false -python-versions = ">=3.7" -files = [ - {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, - {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] -tornado = ["tornado (>=0.2)"] - [[package]] name = "h5py" -version = "3.11.0" +version = "3.12.1" description = "Read and write HDF5 files from Python" optional = true -python-versions = ">=3.8" -files = [ - {file = "h5py-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1625fd24ad6cfc9c1ccd44a66dac2396e7ee74940776792772819fc69f3a3731"}, - {file = "h5py-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c072655ad1d5fe9ef462445d3e77a8166cbfa5e599045f8aa3c19b75315f10e5"}, - {file = "h5py-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77b19a40788e3e362b54af4dcf9e6fde59ca016db2c61360aa30b47c7b7cef00"}, - {file = "h5py-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef4e2f338fc763f50a8113890f455e1a70acd42a4d083370ceb80c463d803972"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007"}, - {file = "h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3"}, - {file = "h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a76cae64080210389a571c7d13c94a1a6cf8cb75153044fd1f822a962c97aeab"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3736fe21da2b7d8a13fe8fe415f1272d2a1ccdeff4849c1421d2fb30fd533bc"}, - {file = "h5py-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6ae84a14103e8dc19266ef4c3e5d7c00b68f21d07f2966f0ca7bdb6c2761fb"}, - {file = "h5py-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:21dbdc5343f53b2e25404673c4f00a3335aef25521bd5fa8c707ec3833934892"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:754c0c2e373d13d6309f408325343b642eb0f40f1a6ad21779cfa9502209e150"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:731839240c59ba219d4cb3bc5880d438248533366f102402cfa0621b71796b62"}, - {file = "h5py-3.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ec9df3dd2018904c4cc06331951e274f3f3fd091e6d6cc350aaa90fa9b42a76"}, - {file = "h5py-3.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:55106b04e2c83dfb73dc8732e9abad69d83a436b5b82b773481d95d17b9685e1"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f4e025e852754ca833401777c25888acb96889ee2c27e7e629a19aee288833f0"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c4b760082626120031d7902cd983d8c1f424cdba2809f1067511ef283629d4b"}, - {file = "h5py-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67462d0669f8f5459529de179f7771bd697389fcb3faab54d63bf788599a48ea"}, - {file = "h5py-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d9c944d364688f827dc889cf83f1fca311caf4fa50b19f009d1f2b525edd33a3"}, - {file = "h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9"}, +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"hdf5\"" +files = [ + {file = "h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda"}, + {file = "h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307"}, + {file = "h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccd9006d92232727d23f784795191bfd02294a4f2ba68708825cb1da39511a93"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad8a76557880aed5234cfe7279805f4ab5ce16b17954606cca90d578d3e713ef"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1473348139b885393125126258ae2d70753ef7e9cec8e7848434f385ae72069e"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:018a4597f35092ae3fb28ee851fdc756d2b88c96336b8480e124ce1ac6fb9166"}, + {file = "h5py-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fdf95092d60e8130ba6ae0ef7a9bd4ade8edbe3569c13ebbaf39baefffc5ba4"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9"}, + {file = "h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f"}, + {file = "h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2b8dd64f127d8b324f5d2cd1c0fd6f68af69084e9e47d27efeb9e28e685af3e"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4532c7e97fbef3d029735db8b6f5bf01222d9ece41e309b20d63cfaae2fb5c4d"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdf6d7936fa824acfa27305fe2d9f39968e539d831c5bae0e0d83ed521ad1ac"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84342bffd1f82d4f036433e7039e241a243531a1d3acd7341b35ae58cdab05bf"}, + {file = "h5py-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:62be1fc0ef195891949b2c627ec06bc8e837ff62d5b911b6e42e38e0f20a897d"}, + {file = "h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf"}, ] [package.dependencies] -numpy = ">=1.17.3" +numpy = ">=1.19.3" [[package]] name = "html5lib" @@ -1069,6 +1018,7 @@ version = "1.1" description = "HTML parser based on the WHATWG HTML specification" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] files = [ {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, @@ -1079,20 +1029,21 @@ six = ">=1.9" webencodings = "*" [package.extras] -all = ["chardet (>=2.2)", "genshi", "lxml"] +all = ["chardet (>=2.2)", "genshi", "lxml ; platform_python_implementation == \"CPython\""] chardet = ["chardet (>=2.2)"] genshi = ["genshi"] -lxml = ["lxml"] +lxml = ["lxml ; platform_python_implementation == \"CPython\""] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.5" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566"}, + {file = "identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc"}, ] [package.extras] @@ -1104,6 +1055,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1118,6 +1070,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -1129,6 +1082,7 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -1138,34 +1092,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "importlib-resources" -version = "6.4.5" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, - {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -1174,31 +1106,22 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." -optional = false -python-versions = ">=3.8" -files = [ - {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, -] - [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -1213,6 +1136,7 @@ version = "4.23.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, @@ -1220,9 +1144,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} jsonschema-specifications = ">=2023.03.6" -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -1232,86 +1154,88 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2023.12.1" +version = "2024.10.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, ] [package.dependencies] -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -1320,6 +1244,7 @@ version = "10.5.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, @@ -1331,6 +1256,7 @@ version = "1.1.0" description = "MessagePack serializer" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, @@ -1404,6 +1330,7 @@ version = "8.4.0" description = "Simple yet flexible natural sorting in Python." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"}, {file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"}, @@ -1419,6 +1346,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1426,39 +1354,67 @@ files = [ [[package]] name = "numpy" -version = "1.24.4" +version = "2.2.1" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308"}, + {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957"}, + {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf"}, + {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2"}, + {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528"}, + {file = "numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95"}, + {file = "numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5"}, + {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73"}, + {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591"}, + {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8"}, + {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0"}, + {file = "numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd"}, + {file = "numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355"}, + {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7"}, + {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d"}, + {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51"}, + {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046"}, + {file = "numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2"}, + {file = "numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348"}, + {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59"}, + {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af"}, + {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51"}, + {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716"}, + {file = "numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e"}, + {file = "numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84"}, + {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631"}, + {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d"}, + {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5"}, + {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71"}, + {file = "numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2"}, + {file = "numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e"}, + {file = "numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918"}, ] [[package]] @@ -1467,6 +1423,7 @@ version = "1.29.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, @@ -1482,6 +1439,7 @@ version = "1.29.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "opentelemetry_sdk-1.29.0-py3-none-any.whl", hash = "sha256:173be3b5d3f8f7d671f20ea37056710217959e774e2749d984355d1f9391a30a"}, {file = "opentelemetry_sdk-1.29.0.tar.gz", hash = "sha256:b0787ce6aade6ab84315302e72bd7a7f2f014b0fb1b7c3295b88afe014ed0643"}, @@ -1498,6 +1456,7 @@ version = "0.50b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "opentelemetry_semantic_conventions-0.50b0-py3-none-any.whl", hash = "sha256:e87efba8fdb67fb38113efea6a349531e75ed7ffc01562f65b802fcecb5e115e"}, {file = "opentelemetry_semantic_conventions-0.50b0.tar.gz", hash = "sha256:02dc6dbcb62f082de9b877ff19a3f1ffaa3c306300fa53bfac761c4567c83d38"}, @@ -1513,6 +1472,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1520,62 +1480,90 @@ files = [ [[package]] name = "pandas" -version = "1.5.3" +version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, - {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, - {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, - {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, - {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, - {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, - {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, - {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, ] [package.dependencies] numpy = [ - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] -python-dateutil = ">=2.8.1" +python-dateutil = ">=2.8.2" pytz = ">=2020.1" +tzdata = ">=2022.7" [package.extras] -test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] - -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] [[package]] name = "platformdirs" @@ -1583,6 +1571,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1599,6 +1588,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1614,6 +1604,7 @@ version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, @@ -1632,6 +1623,7 @@ version = "1.25.0" description = "Beautiful, Pythonic protocol buffers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961"}, {file = "proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"}, @@ -1645,22 +1637,23 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.29.1" +version = "5.29.3" description = "" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110"}, - {file = "protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34"}, - {file = "protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18"}, - {file = "protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155"}, - {file = "protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d"}, - {file = "protobuf-5.29.1-cp38-cp38-win32.whl", hash = "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853"}, - {file = "protobuf-5.29.1-cp38-cp38-win_amd64.whl", hash = "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331"}, - {file = "protobuf-5.29.1-cp39-cp39-win32.whl", hash = "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57"}, - {file = "protobuf-5.29.1-cp39-cp39-win_amd64.whl", hash = "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c"}, - {file = "protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0"}, - {file = "protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb"}, + {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, + {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, + {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, + {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, + {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, + {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, + {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, + {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, + {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, ] [[package]] @@ -1669,6 +1662,7 @@ version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] files = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1680,6 +1674,7 @@ version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -1691,6 +1686,7 @@ version = "0.4.1" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, @@ -1705,6 +1701,7 @@ version = "8.0.4" description = "The kitchen sink of Python utility libraries for doing \"stuff\" in a functional way. Based on the Lo-Dash Javascript library." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydash-8.0.4-py3-none-any.whl", hash = "sha256:59d0c9ca0d22b4f8bcfab01bfe2e89b49f4c9e9fa75961caf156094670260999"}, {file = "pydash-8.0.4.tar.gz", hash = "sha256:a33fb17b4b06c617da5c57c711605d2dc8723311ee5388c8371f87cd44bf4112"}, @@ -1718,13 +1715,14 @@ dev = ["build", "coverage", "furo", "invoke", "mypy", "pytest", "pytest-cov", "p [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -1736,6 +1734,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -1758,6 +1757,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1772,6 +1772,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -1786,6 +1787,7 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1797,6 +1799,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1859,6 +1862,7 @@ version = "0.35.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, @@ -1874,6 +1878,7 @@ version = "2022.3.2" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "regex-2022.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab69b4fe09e296261377d209068d52402fb85ef89dc78a9ac4a29a895f4e24a7"}, {file = "regex-2022.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5bc5f921be39ccb65fdda741e04b2555917a4bced24b4df14eddc7569be3b493"}, @@ -1957,6 +1962,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1974,114 +1980,115 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rpds-py" -version = "0.20.1" +version = "0.22.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"}, - {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899"}, - {file = "rpds_py-0.20.1-cp310-none-win32.whl", hash = "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff"}, - {file = "rpds_py-0.20.1-cp310-none-win_amd64.whl", hash = "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711"}, - {file = "rpds_py-0.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75"}, - {file = "rpds_py-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3"}, - {file = "rpds_py-0.20.1-cp311-none-win32.whl", hash = "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732"}, - {file = "rpds_py-0.20.1-cp311-none-win_amd64.whl", hash = "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84"}, - {file = "rpds_py-0.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17"}, - {file = "rpds_py-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5"}, - {file = "rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c"}, - {file = "rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb"}, - {file = "rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e"}, - {file = "rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb"}, - {file = "rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782"}, - {file = "rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e"}, - {file = "rpds_py-0.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191"}, - {file = "rpds_py-0.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc"}, - {file = "rpds_py-0.20.1-cp38-none-win32.whl", hash = "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1"}, - {file = "rpds_py-0.20.1-cp38-none-win_amd64.whl", hash = "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425"}, - {file = "rpds_py-0.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad"}, - {file = "rpds_py-0.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf"}, - {file = "rpds_py-0.20.1-cp39-none-win32.whl", hash = "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca"}, - {file = "rpds_py-0.20.1-cp39-none-win_amd64.whl", hash = "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a"}, - {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, + {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, + {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, ] [[package]] @@ -2090,6 +2097,7 @@ version = "4.9" description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" +groups = ["main"] files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -2100,13 +2108,14 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruamel-yaml" -version = "0.18.6" +version = "0.18.10" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, + {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, + {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, ] [package.dependencies] @@ -2118,61 +2127,54 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] name = "ruamel-yaml-clib" -version = "0.2.8" +version = "0.2.12" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" optional = false -python-versions = ">=3.6" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" +files = [ + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, + {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, ] [[package]] @@ -2181,6 +2183,7 @@ version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, @@ -2204,23 +2207,24 @@ files = [ [[package]] name = "setuptools" -version = "75.3.0" +version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, - {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -2228,6 +2232,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2239,6 +2244,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -2250,6 +2256,7 @@ version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, @@ -2257,25 +2264,25 @@ files = [ [[package]] name = "sphinx" -version = "7.1.2" +version = "7.3.7" description = "Python documentation generator" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, - {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" +alabaster = ">=0.7.14,<0.8.0" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.21" +docutils = ">=0.18.1,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.13" +Pygments = ">=2.14" requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" @@ -2283,31 +2290,33 @@ sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] [[package]] name = "sphinx-autodoc-typehints" -version = "2.0.1" +version = "2.3.0" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149"}, - {file = "sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12"}, + {file = "sphinx_autodoc_typehints-2.3.0-py3-none-any.whl", hash = "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67"}, + {file = "sphinx_autodoc_typehints-2.3.0.tar.gz", hash = "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084"}, ] [package.dependencies] -sphinx = ">=7.1.2" +sphinx = ">=7.3.5" [package.extras] docs = ["furo (>=2024.1.29)"] numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.4.2)", "diff-cover (>=8.0.3)", "pytest (>=8.0.1)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.9)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] [[package]] name = "sphinx-jinja2-compat" @@ -2315,6 +2324,7 @@ version = "0.3.0" description = "Patches Jinja2 v3 to restore compatibility with earlier Sphinx versions." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "sphinx_jinja2_compat-0.3.0-py3-none-any.whl", hash = "sha256:b1e4006d8e1ea31013fa9946d1b075b0c8d2a42c6e3425e63542c1e9f8be9084"}, {file = "sphinx_jinja2_compat-0.3.0.tar.gz", hash = "sha256:f3c1590b275f42e7a654e081db5e3e5fb97f515608422bde94015ddf795dfe7c"}, @@ -2327,17 +2337,20 @@ standard-imghdr = {version = "3.10.14", markers = "python_version >= \"3.13\""} [[package]] name = "sphinx-prompt" -version = "1.5.0" +version = "1.8.0" description = "Sphinx directive to add unselectable prompt" optional = false -python-versions = "*" +python-versions = ">=3.9,<4.0" +groups = ["dev"] files = [ - {file = "sphinx_prompt-1.5.0-py3-none-any.whl", hash = "sha256:fa4e90d8088b5a996c76087d701fc7e31175f8b9dc4aab03a507e45051067162"}, + {file = "sphinx_prompt-1.8.0-py3-none-any.whl", hash = "sha256:369ecc633f0711886f9b3a078c83264245be1adf46abeeb9b88b5519e4b51007"}, + {file = "sphinx_prompt-1.8.0.tar.gz", hash = "sha256:47482f86fcec29662fdfd23e7c04ef03582714195d01f5d565403320084372ed"}, ] [package.dependencies] +docutils = "*" pygments = "*" -Sphinx = "*" +Sphinx = ">=7.0.0,<8.0.0" [[package]] name = "sphinx-rtd-theme" @@ -2345,6 +2358,7 @@ version = "1.3.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["dev"] files = [ {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, @@ -2364,6 +2378,7 @@ version = "3.4.5" description = "Tabbed views for Sphinx" optional = false python-versions = "~=3.7" +groups = ["dev"] files = [ {file = "sphinx-tabs-3.4.5.tar.gz", hash = "sha256:ba9d0c1e3e37aaadd4b5678449eb08176770e0fc227e769b6ce747df3ceea531"}, {file = "sphinx_tabs-3.4.5-py3-none-any.whl", hash = "sha256:92cc9473e2ecf1828ca3f6617d0efc0aa8acb06b08c56ba29d1413f2f0f6cf09"}, @@ -2384,6 +2399,7 @@ version = "3.8.1" description = "Box of handy tools for Sphinx 🧰 📔" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "sphinx_toolbox-3.8.1-py3-none-any.whl", hash = "sha256:53d8e77dd79e807d9ef18590c4b2960a5aa3c147415054b04c31a91afed8b88b"}, {file = "sphinx_toolbox-3.8.1.tar.gz", hash = "sha256:a4b39a6ea24fc8f10e24f052199bda17837a0bf4c54163a56f521552395f5e1a"}, @@ -2414,47 +2430,53 @@ testing = ["coincidence (>=0.4.3)", "pygments (>=2.7.4,<=2.13.0)"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] @@ -2463,6 +2485,7 @@ version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" +groups = ["dev"] files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, @@ -2477,6 +2500,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -2487,32 +2511,36 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] @@ -2521,6 +2549,8 @@ version = "3.10.14" description = "Standard library imghdr redistribution. \"dead battery\"." optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version >= \"3.13\"" files = [ {file = "standard_imghdr-3.10.14-py3-none-any.whl", hash = "sha256:cdf6883163349624dee9a81d2853a20260337c4cd41c04e99c082e01833a08e2"}, {file = "standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52"}, @@ -2532,6 +2562,7 @@ version = "1.2.0" description = "String case converter." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, ] @@ -2542,6 +2573,7 @@ version = "0.9.0" description = "Pretty-print tabular data" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -2556,6 +2588,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2597,6 +2631,7 @@ version = "3.28.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +groups = ["dev"] files = [ {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, @@ -2614,7 +2649,7 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3) ; python_version < \"3.4\"", "psutil (>=5.6.1) ; platform_python_implementation == \"cpython\"", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "twined" @@ -2622,6 +2657,7 @@ version = "0.6.0" description = "A library to help digital twins and data services talk to one another." optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ {file = "twined-0.6.0-py3-none-any.whl", hash = "sha256:5f9d29c02bdeae7bb7ead80aafc1c650eb642fc864375fab529045ed53067aa1"}, {file = "twined-0.6.0.tar.gz", hash = "sha256:d7709a760f14f29946651a3a69abc28ae312f865cbe7d937d0afc427de34664a"}, @@ -2637,6 +2673,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2648,6 +2685,7 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["dev"] files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -2659,13 +2697,13 @@ version = "5.2" description = "tzinfo object for the local timezone" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] @@ -2673,30 +2711,32 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev"] files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.28.0" +version = "20.28.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, - {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, + {file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, + {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, ] [package.dependencies] @@ -2706,7 +2746,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "webencodings" @@ -2714,34 +2754,19 @@ version = "0.5.1" description = "Character encoding aliases for legacy web content" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -[[package]] -name = "werkzeug" -version = "3.0.6" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"}, - {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - [[package]] name = "wrapt" version = "1.17.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, @@ -2812,27 +2837,28 @@ files = [ [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] hdf5 = ["h5py"] [metadata] -lock-version = "2.0" -python-versions = "^3.8" -content-hash = "5539fa069affa26830f3a929dd3d0ae97af9ccad05b0a434466f415ca7c9c6a3" +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "f5f9a73e20b613a5064ff065a5f314e9d228191730139f3ffdc5d476987dcd79" diff --git a/pyproject.toml b/pyproject.toml index 698488848..30f74ea70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "octue" -version = "0.61.2" +version = "0.62.0" description = "A package providing template applications for data services, and a python SDK to the Octue API." readme = "README.md" authors = ["Marcus Lugg ", "Thomas Clark "] @@ -13,24 +13,22 @@ classifiers = [ "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", ] [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" click = ">=7,<9" coolname = "^2" -Flask = "^2" google-auth = ">=1.27.0,<3" google-cloud-pubsub = "^2.5" google-cloud-secret-manager = "^2.20" google-cloud-storage = ">=1.35.1, <3" google-crc32c = "^1.1" -gunicorn = "^22" python-dateutil = "^2.8" pyyaml = "^6" h5py = { version = "^3.6", optional = true } @@ -54,10 +52,10 @@ tox = "^3.23" pre-commit = "^2.17" coverage = "^5" # Template app dependencies -numpy = "^1" +numpy = "^2.2.1" +pandas = "^2.2.3" dateparser = "1.1.1" stringcase = "1.2.0" -pandas = "^1.3" # Documentation Sphinx = ">=5,<8" sphinx-rtd-theme = ">=1,<2" @@ -67,16 +65,24 @@ ruff = "^0.6.9" [tool.ruff] line-length = 120 -# Enable pycodestyle (`E`) and Pyflakes (`F`) codes. -lint.select = ["E", "F"] + +[tool.ruff.lint] +# Enable pydocstyle (`D`), pycodestyle (`E`), and Pyflakes (`F`) codes. +select = ["D", "E", "F"] # Ignore E501 line-too-long - see https://docs.astral.sh/ruff/faq/#is-the-ruff-linter-compatible-with-black for why -lint.ignore = ["F405", "E501", "E203", "E731", "N818"] +ignore = [ + "D100", "D101", "D102", "D103", "D104", "D105", "D107", "D203", "D205", "D213", "D400", "D415", + "E501", "E203", "E731", + "F405", + "N818", +] [tool.ruff.lint.isort] known-first-party = ["octue", "app", "fractal", "test", "tests", "twined"] section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] force-sort-within-sections = true + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d3f6f4e77..000000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[pydocstyle] -ignore = D100, D101, D102, D103, D104, D105, D107, D203, D205, D213, D400, D415 diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl index 9f6af285e..6bfb5f24a 100644 --- a/terraform/.terraform.lock.hcl +++ b/terraform/.terraform.lock.hcl @@ -2,21 +2,40 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { - version = "4.53.1" - constraints = "4.53.1" + version = "6.23.0" + constraints = "~> 6.12" hashes = [ - "h1:2I17Y3hcJ0TQ/yiUB8fRhB5FE3Ula+vGZecjGmq+NdU=", - "zh:29e289c3026d369e8b06b0dbe7f35db4aaa08439e801ddfc3349bd28fbf93635", - "zh:3e43a212acd2710f9cebd24677ff67c7aedffd958914c06eb7ca147556ee95f6", - "zh:53120dee6a3a29ac9559e23630a92350ba2ad20b1f8410be2fa2956d60769e2c", - "zh:63e7fd9d5da42db8421e53fc98e1bd91ad02e348b304255cc1e4bf7d55d84d06", - "zh:8084389a193262ddfe3f0367ef6660bc292799b0cbcc4196b8148d23c827b6c7", - "zh:86e5809b4a042a0161afcdc4c84a1082ea9461557affcd482ff740c5e252187f", - "zh:99ff6325e2bf07430aa50637078b33904a9f08b6a11d1370a133eb9902a855ba", - "zh:cbdd1415abd7034ff8e4a5173e1d0e13d2364185752df277bbfd34cc8242ba23", - "zh:d42ed1eaa3019d23af86f253ee38204d3b5d020c1daee50be0d8bbe4d9170eaf", - "zh:ee8b3371858b083f10dfb6c31db7933d7dfb3a8b6e6de6669f1fe07f9c407a1e", + "h1:Gr39ABNw+A6lwP2gPG+yCzGmU5T97iI5qT0XLCd3Dh4=", + "zh:032dd78eff887a673a1067008a8e47a69983bbcea9f41832320470247a76863a", + "zh:1af89f75142cf9c54499a466c8dc7055e2bbf02771a6b8c8cd57eb13dce9a800", + "zh:3696a8e72c6cef80fec3c3574fc8519f0410f23f6dc3e3540d2f03345c140d38", + "zh:58a15c71ae128ff64117c1c6b9ccf8ab2ac3e8f9c2c52957d8327f93495f62b1", + "zh:70ba2909611e8d1cc8009567e50e195c4269e6582d6a6fa0bce0d4e6313ab8d5", + "zh:8f8489d1eb8c189d59dc85e519e51ab4c4b1940e4d72450ae130ba752028fa01", + "zh:99c8c4e8dc67a7ab597d46ed566f64c4409761276f34bd863457a37254620fa6", + "zh:9b24d53440e8d7e06020e7b3aeca0dde4f1a3ef997d7da05ff3acb918896fcc2", + "zh:b3667fd6057997dbf0bd0179ddf686c272d4ab4fc7da6a03fdf2fad31ad4ecb5", + "zh:cc6df6d2291a337a5f434b7335c693164c6987604b9690d6b953b796d8eaa08a", + "zh:d871f39c3c5b63995793c9a70f107e52ca699d211c770a6e7ebc398ba59bcdc7", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:f745d0071e8acdc13995c8f9ecb34d2309303134ae361b17f3ed31399d20a165", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.12.1" + hashes = [ + "h1:JzYsPugN8Fb7C4NlfLoFu7BBPuRVT2/fCOdCaxshveI=", + "zh:090023137df8effe8804e81c65f636dadf8f9d35b79c3afff282d39367ba44b2", + "zh:26f1e458358ba55f6558613f1427dcfa6ae2be5119b722d0b3adb27cd001efea", + "zh:272ccc73a03384b72b964918c7afeb22c2e6be22460d92b150aaf28f29a7d511", + "zh:438b8c74f5ed62fe921bd1078abe628a6675e44912933100ea4fa26863e340e9", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:85c8bd8eefc4afc33445de2ee7fbf33a7807bc34eb3734b8eefa4e98e4cddf38", + "zh:98bbe309c9ff5b2352de6a047e0ec6c7e3764b4ed3dfd370839c4be2fbfff869", + "zh:9c7bf8c56da1b124e0e2f3210a1915e778bab2be924481af684695b52672891e", + "zh:d2200f7f6ab8ecb8373cda796b864ad4867f5c255cff9d3b032f666e4c78f625", + "zh:d8c7926feaddfdc08d5ebb41b03445166df8c125417b28d64712dccd9feef136", + "zh:e2412a192fc340c61b373d6c20c9d805d7d3dee6c720c34db23c2a8ff0abd71b", + "zh:e6ac6bba391afe728a099df344dbd6481425b06d61697522017b8f7a59957d44", ] } diff --git a/terraform/artifact_registry.tf b/terraform/artifact_registry.tf deleted file mode 100644 index 87f8e28ea..000000000 --- a/terraform/artifact_registry.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "google_artifact_registry_repository" "artifact_registry_repository" { - location = var.region - repository_id = "${var.service_namespace}" - description = "Docker image repository" - format = "DOCKER" -} diff --git a/terraform/bigquery.tf b/terraform/bigquery.tf deleted file mode 100644 index bd8bfa630..000000000 --- a/terraform/bigquery.tf +++ /dev/null @@ -1,100 +0,0 @@ -resource "google_bigquery_dataset" "test_dataset" { - dataset_id = "octue_sdk_python_test_dataset" - description = "A dataset for testing storing events for the Octue SDK." - location = "EU" - - labels = { - env = "default" - } -} - -resource "google_bigquery_table" "test_table" { - dataset_id = google_bigquery_dataset.test_dataset.dataset_id - table_id = "service-events" - clustering = ["sender", "question_uuid"] - - schema = < Settings > Users -# and Permissions, with "Owner" level permission. - -resource "google_service_account" "dev_cortadocodes_service_account" { - account_id = "dev-cortadocodes" - description = "Allow cortadocodes to access developer-specific resources" - display_name = "dev-cortadocodes" - project = var.project -} - - -resource "google_service_account" "github_actions_service_account" { - account_id = "github-actions" - description = "Allow GitHub Actions to test the SDK." - display_name = "github-actions" - project = var.project -} diff --git a/terraform/main.tf b/terraform/main.tf index e858236dd..2e2a41694 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -1,100 +1,50 @@ terraform { + required_version = ">= 1.8.0" + required_providers { google = { source = "hashicorp/google" - version = "4.53.1" + version = "~>6.12" } } + cloud { organization = "octue" workspaces { - name = "octue-sdk-python" + project = "octue-twined" + tags = ["testing"] } } } -resource "google_project_service" "pub_sub" { - project = var.project - service = "pubsub.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "cloud_resource_manager" { - project = var.project - service = "cloudresourcemanager.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "iam" { - project = var.project - service = "iam.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "artifact_registry" { - project = var.project - service = "artifactregistry.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "cloud_run" { - project = var.project - service = "run.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "cloud_functions" { - project = var.project - service = "cloudfunctions.googleapis.com" +provider "google" { + project = var.google_cloud_project_id + region = var.google_cloud_region } -resource "google_project_service" "eventarc" { - project = var.project - service = "eventarc.googleapis.com" -} +data "google_client_config" "default" {} -resource "google_project_service" "cloud_build" { - project = var.project - service = "cloudbuild.googleapis.com" +module "octue_twined_core" { + source = "git::github.com/octue/terraform-octue-twined-core.git?ref=0.1.1" + google_cloud_project_id = var.google_cloud_project_id + google_cloud_region = var.google_cloud_region + github_account = var.github_account + maintainer_service_account_names = var.maintainer_service_account_names + deletion_protection = var.deletion_protection } -resource "google_project_service" "bigquery" { - project = var.project - service = "bigquery.googleapis.com" +resource "google_project_service" "pub_sub" { + service = "pubsub.googleapis.com" + disable_dependent_services = true + project = var.google_cloud_project_id } -provider "google" { - credentials = file(var.credentials_file) - project = var.project - region = var.region +resource "google_pubsub_topic" "services_topic" { + name = "main.octue.services" + depends_on = [google_project_service.pub_sub] } diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 000000000..1683d9c23 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,10 @@ +output "event_store_id" { + value = module.octue_twined_core.event_store_id + description = "The full ID of the BigQuery table acting as the Octue Twined services event store." +} + + +output "storage_bucket_url" { + value = module.octue_twined_core.storage_bucket_url + description = "The `gs://` URL of the storage bucket used to store service inputs, outputs, and diagnostics." +} diff --git a/terraform/pub_sub.tf b/terraform/pub_sub.tf deleted file mode 100644 index ce436afec..000000000 --- a/terraform/pub_sub.tf +++ /dev/null @@ -1,3 +0,0 @@ -resource "google_pubsub_topic" "services_topic" { - name = "octue.services" -} diff --git a/terraform/variables.tf b/terraform/variables.tf index ccdfb938c..1141c9510 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -1,39 +1,28 @@ -variable "organization" { +variable "google_cloud_project_id" { type = string - default = "octue" -} - -variable "project" { - type = string default = "octue-sdk-python" } -variable "project_number" { - type = string - default = "437801218871" -} -variable "region" { +variable "google_cloud_region" { type = string - default = "europe-west1" + default = "europe-west9" } -variable "github_organisation" { - type = string + +variable "github_account" { + type = string default = "octue" } -variable "credentials_file" { - type = string - default = "gcp-credentials.json" -} -variable "service_namespace" { - type = string - default = "octue" +variable "maintainer_service_account_names" { + type = set(string) + default = ["cortadocodes", "thclark"] } -variable "service_name" { - type = string - default = "example-service-cloud-run" + +variable "deletion_protection" { + type = bool + default = false } diff --git a/tests/base.py b/tests/base.py index ecf7e42dc..10c72e793 100644 --- a/tests/base.py +++ b/tests/base.py @@ -8,7 +8,7 @@ from octue.cloud.emulators._pub_sub import MockTopic from octue.cloud.emulators.cloud_storage import GoogleCloudStorageEmulatorTestResultModifier from octue.cloud.emulators.service import ServicePatcher -from octue.cloud.events import OCTUE_SERVICES_PREFIX +from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME from octue.cloud.storage import GoogleCloudStorageClient from octue.resources import Datafile, Dataset, Manifest from tests import TEST_BUCKET_NAME, TEST_PROJECT_NAME @@ -23,7 +23,7 @@ def startTestRun(self): super().startTestRun() with ServicePatcher(): - self.services_topic = MockTopic(name=OCTUE_SERVICES_PREFIX, project_name=TEST_PROJECT_NAME) + self.services_topic = MockTopic(name=OCTUE_SERVICES_TOPIC_NAME, project_name=TEST_PROJECT_NAME) self.services_topic.create(allow_existing=True) diff --git a/tests/cloud/deployment/__init__.py b/tests/cloud/deployment/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/cloud/deployment/google/__init__.py b/tests/cloud/deployment/google/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/cloud/deployment/google/cloud_run/__init__.py b/tests/cloud/deployment/google/cloud_run/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/cloud/deployment/google/cloud_run/test_cloud_run_deployment.py b/tests/cloud/deployment/google/cloud_run/test_cloud_run_deployment.py deleted file mode 100644 index 056d67a91..000000000 --- a/tests/cloud/deployment/google/cloud_run/test_cloud_run_deployment.py +++ /dev/null @@ -1,72 +0,0 @@ -import os -import time -import unittest -from unittest import TestCase - -import twined.exceptions -from octue.cloud.events.replayer import EventReplayer -from octue.cloud.events.validation import is_event_valid -from octue.cloud.pub_sub.bigquery import get_events -from octue.resources import Child - - -EXAMPLE_SERVICE_SRUID = "octue/example-service:0.5.0" - - -@unittest.skipUnless( - condition=os.getenv("RUN_CLOUD_RUN_DEPLOYMENT_TEST", "0").lower() == "1", - reason="'RUN_CLOUD_RUN_DEPLOYMENT_TEST' environment variable is False or not present.", -) -class TestCloudRunDeployment(TestCase): - # This is the service ID of the example service deployed to Google Cloud Run. - child = Child( - id=EXAMPLE_SERVICE_SRUID, - backend={"name": "GCPPubSubBackend", "project_name": os.environ["TEST_PROJECT_NAME"]}, - ) - - def test_forwards_exceptions_to_parent(self): - """Test that exceptions raised in the (remote) responding service are forwarded to and raised by the asker.""" - with self.assertRaises(twined.exceptions.InvalidValuesContents): - self.child.ask(input_values={"invalid_input_data": "hello"}) - - def test_synchronous_question(self): - """Test that the Google Cloud Run example deployment works, providing a service that can be asked questions and - send responses. - """ - answer, _ = self.child.ask(input_values={"n_iterations": 3}) - - # Check the output values. - self.assertEqual(answer["output_values"], [1, 2, 3, 4, 5]) - - # Check that the output dataset and its files can be accessed. - with answer["output_manifest"].datasets["example_dataset"].files.one() as (datafile, f): - self.assertEqual(f.read(), "This is some example service output.") - - def test_asynchronous_question(self): - """Test asking an asynchronous question and retrieving the resulting events from the event store.""" - answer, question_uuid = self.child.ask(input_values={"n_iterations": 3}, asynchronous=True) - self.assertIsNone(answer) - - # Wait for question to complete. - time.sleep(15) - - events = get_events(table_id="octue_sdk_python_test_dataset.service-events", question_uuid=question_uuid) - - self.assertTrue( - is_event_valid( - event=events[0]["event"], - attributes=events[0]["attributes"], - recipient=None, - parent_sdk_version=None, - child_sdk_version=None, - ) - ) - - replayer = EventReplayer() - answer = replayer.handle_events(events) - - # Check the output values. - self.assertEqual(list(answer["output_values"]), [1, 2, 3, 4, 5]) - - with answer["output_manifest"].datasets["example_dataset"].files.one() as (datafile, f): - self.assertEqual(f.read(), "This is some example service output.") diff --git a/tests/cloud/deployment/google/cloud_run/test_flask_app.py b/tests/cloud/deployment/google/cloud_run/test_flask_app.py deleted file mode 100644 index bef35f05a..000000000 --- a/tests/cloud/deployment/google/cloud_run/test_flask_app.py +++ /dev/null @@ -1,239 +0,0 @@ -import copy -import logging -import os -import uuid -from unittest import TestCase -from unittest.mock import patch - -from google.api_core.exceptions import NotFound - -from octue.cloud.deployment.google.cloud_run import flask_app -from octue.configuration import ServiceConfiguration -from octue.utils.patches import MultiPatcher -from tests import TESTS_DIR - - -flask_app.app.testing = True - - -TWINE_FILE_PATH = os.path.join(TESTS_DIR, "data", "twines", "valid_schema_twine.json") - -MOCK_CONFIGURATION = ServiceConfiguration( - namespace="testing", - name="test-app", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_module"), - twine_path=TWINE_FILE_PATH, - app_configuration_path="blah.json", - event_store_table_id="mock-event-store-table-id", -) - - -class TestInvalidPayloads(TestCase): - def test_post_to_index_with_no_payload_results_in_400_error(self): - """Test that a 400 (bad request) error code is returned if no payload is sent to the Flask endpoint.""" - with flask_app.app.test_client() as client: - response = client.post("/", json={"deliveryAttempt": 1}) - self.assertEqual(response.status_code, 400) - - def test_post_to_index_with_invalid_payload_results_in_400_error(self): - """Test that a 400 (bad request) error code is returned if an invalid payload is sent to the Flask endpoint.""" - with flask_app.app.test_client() as client: - response = client.post("/", json={"some": "data", "deliveryAttempt": 1}) - self.assertEqual(response.status_code, 400) - - response = client.post("/", json={"message": "data", "deliveryAttempt": 1}) - self.assertEqual(response.status_code, 400) - - -class TestQuestionRedelivery(TestCase): - def test_warning_logged_if_no_event_store_provided(self): - """Test that the question is allowed to proceed to analysis and a warning is logged if the event store cannot be - checked because one hasn't been specified in the service configuration. - """ - mock_configuration = copy.deepcopy(MOCK_CONFIGURATION) - mock_configuration.event_store_table_id = None - - with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: - with patch("octue.configuration.ServiceConfiguration.from_file", return_value=mock_configuration): - with self.assertLogs(level=logging.WARNING) as logging_context: - response = client.post( - "/", - json={ - "deliveryAttempt": 1, - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": str(uuid.uuid4()), - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertTrue( - logging_context.output[0].endswith( - "Cannot check if question has been redelivered as the 'event_store_table_id' key hasn't been set in " - "the service configuration (`octue.yaml` file)." - ) - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() - - def test_warning_logged_if_event_store_not_found(self): - """Test that the question is allowed to proceed to analysis and a warning is logged if the event store cannot be - found. - """ - mock_configuration = copy.deepcopy(MOCK_CONFIGURATION) - mock_configuration.event_store_table_id = "nonexistent.table" - - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=mock_configuration), - patch("octue.cloud.deployment.google.cloud_run.flask_app.get_events", side_effect=NotFound("blah")), - ] - ) - - with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: - with multi_patcher: - with self.assertLogs(level=logging.WARNING) as logging_context: - response = client.post( - "/", - json={ - "deliveryAttempt": 1, - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": str(uuid.uuid4()), - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertTrue( - logging_context.output[0].endswith( - "Cannot check if question has been redelivered as no event store table was found with the ID " - "'nonexistent.table'; check that the 'event_store_table_id' key in the service configuration " - "(`octue.yaml` file) is correct." - ) - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() - - def test_new_question(self): - """Test that a new question is checked against the event store and allowed to proceed to analysis.""" - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATION), - patch("octue.cloud.deployment.google.cloud_run.flask_app.get_events", return_value=[]), - ] - ) - - with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: - with multi_patcher: - with self.assertLogs() as logging_context: - response = client.post( - "/", - json={ - "deliveryAttempt": 1, - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": str(uuid.uuid4()), - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertTrue(logging_context.output[0].endswith("is a new question.")) - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() - - def test_redelivered_questions_are_acknowledged_and_dropped(self): - """Test that questions undesirably redelivered by Pub/Sub are acknowledged and dropped.""" - question_uuid = "fcd7aad7-dbf0-47d2-8984-220d493df2c1" - - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATION), - patch( - "octue.cloud.deployment.google.cloud_run.flask_app.get_events", - return_value=[{"attributes": {"retry_count": "0"}}], - ), - ] - ) - - with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: - with self.assertLogs(level=logging.WARNING) as logging_context: - with multi_patcher: - response = client.post( - "/", - json={ - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": question_uuid, - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertIn( - "has already been received by the service. It will now be acknowledged and dropped to prevent further " - "redundant redelivery.", - logging_context.output[0], - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_not_called() - - def test_retried_questions_are_allowed(self): - """Test that questions explicitly retried by the SDK are allowed to proceed to analysis.""" - question_uuid = "fcd7aad7-dbf0-47d2-8984-220d493df2c1" - - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATION), - patch( - "octue.cloud.deployment.google.cloud_run.flask_app.get_events", - return_value=[{"attributes": {"retry_count": "0"}}], - ), - ] - ) - - with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: - with multi_patcher: - response = client.post( - "/", - json={ - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": question_uuid, - "forward_logs": "1", - "retry_count": "1", - }, - }, - }, - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() diff --git a/tests/cloud/deployment/google/test_answer_pub_sub_question.py b/tests/cloud/deployment/google/test_answer_pub_sub_question.py deleted file mode 100644 index 37513b674..000000000 --- a/tests/cloud/deployment/google/test_answer_pub_sub_question.py +++ /dev/null @@ -1,131 +0,0 @@ -import json -import os -from unittest import TestCase, mock -from unittest.mock import patch - -import yaml - -from octue.cloud.deployment.google.answer_pub_sub_question import answer_question -from octue.cloud.emulators._pub_sub import MockTopic -from octue.utils.patches import MultiPatcher -from tests.mocks import MockOpen - - -class TestAnswerPubSubQuestion(TestCase): - def test_with_no_app_configuration_file(self): - """Test that the `answer_question` function uses the default service and app configuration values when the - minimal service configuration is provided with no path to an app configuration file. - """ - with MultiPatcher( - patches=[ - patch( - "octue.configuration.open", - mock.mock_open( - read_data=yaml.dump({"services": [{"name": "test-service", "namespace": "testing"}]}) - ), - ), - patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), - patch("octue.cloud.deployment.google.answer_pub_sub_question.Service"), - patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), - ] - ): - with patch( - "octue.cloud.deployment.google.answer_pub_sub_question.Runner.from_configuration" - ) as mock_constructor: - answer_question( - question={ - "data": {}, - "attributes": { - "question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "parent_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "originator_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "parent": "some/originator:service", - "originator": "some/originator:service", - "retry_count": 0, - }, - }, - project_name="a-project-name", - ) - - self.assertTrue( - mock_constructor.call_args.kwargs["service_configuration"].app_source_path.endswith("octue-sdk-python") - ) - self.assertTrue(mock_constructor.call_args.kwargs["service_configuration"].twine_path.endswith("twine.json")) - self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].diagnostics_cloud_path) - self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].service_registries) - - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].configuration_values) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].configuration_manifest) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].children) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].output_location) - - self.assertEqual(mock_constructor.call_args.kwargs["project_name"], "a-project-name") - self.assertEqual(mock_constructor.call_args.kwargs["service_id"], "testing/test-service:blah") - - def test_with_service_configuration_file_and_app_configuration_file(self): - """Test that the `answer_question` function uses the values in the service and app configuration files if they - are provided. - """ - - class MockOpenForConfigurationFiles(MockOpen): - path_to_contents_mapping = { - "octue.yaml": yaml.dump( - { - "services": [ - { - "name": "test-service", - "namespace": "testing", - "app_source_path": "/path/to/app_dir", - "twine_path": "path/to/twine.json", - "app_configuration_path": "app_configuration.json", - } - ] - } - ), - "app_configuration.json": json.dumps({"configuration_values": {"hello": "configuration"}}), - } - - with patch( - "octue.cloud.deployment.google.answer_pub_sub_question.Runner.from_configuration" - ) as mock_constructor: - with MultiPatcher( - patches=[ - patch("octue.configuration.open", mock.mock_open(mock=MockOpenForConfigurationFiles)), - patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), - patch("octue.cloud.deployment.google.answer_pub_sub_question.Service"), - patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), - ] - ): - answer_question( - question={ - "data": {}, - "attributes": { - "question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "parent_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "originator_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "parent": "some/originator:service", - "originator": "some/originator:service", - "retry_count": 0, - }, - }, - project_name="a-project-name", - ) - - self.assertTrue( - mock_constructor.call_args.kwargs["service_configuration"].app_source_path.endswith("path/to/app_dir") - ) - self.assertTrue( - mock_constructor.call_args.kwargs["service_configuration"].twine_path.endswith("path/to/twine.json") - ) - self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].diagnostics_cloud_path) - self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].service_registries) - - self.assertEqual( - mock_constructor.call_args.kwargs["app_configuration"].configuration_values, {"hello": "configuration"} - ) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].configuration_manifest) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].children) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].output_location) - - self.assertEqual(mock_constructor.call_args.kwargs["project_name"], "a-project-name") - self.assertEqual(mock_constructor.call_args.kwargs["service_id"], "testing/test-service:blah") diff --git a/tests/cloud/events/test_answer_question.py b/tests/cloud/events/test_answer_question.py new file mode 100644 index 000000000..dd0e52c31 --- /dev/null +++ b/tests/cloud/events/test_answer_question.py @@ -0,0 +1,85 @@ +import json +import os +from unittest import TestCase, mock +from unittest.mock import patch + +import yaml + +from octue.cloud.emulators._pub_sub import MockTopic +from octue.cloud.events.answer_question import answer_question +from octue.configuration import load_service_and_app_configuration +from octue.utils.patches import MultiPatcher +from tests.mocks import MockOpen + + +class TestAnswerQuestion(TestCase): + def test_answer_question(self): + """Test that the `answer_question` function uses the values in the service and app configurations correctly.""" + + class MockOpenForConfigurationFiles(MockOpen): + path_to_contents_mapping = { + "octue.yaml": yaml.dump( + { + "services": [ + { + "name": "test-service", + "namespace": "testing", + "app_source_path": "/path/to/app_dir", + "twine_path": "path/to/twine.json", + "app_configuration_path": "app_configuration.json", + } + ] + } + ), + "app_configuration.json": json.dumps({"configuration_values": {"hello": "configuration"}}), + } + + with patch("octue.cloud.events.answer_question.Runner.from_configuration") as mock_constructor: + with MultiPatcher( + patches=[ + patch("octue.configuration.open", mock.mock_open(mock=MockOpenForConfigurationFiles)), + patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), + patch("octue.cloud.events.answer_question.Service"), + patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), + ] + ): + service_config, app_config = load_service_and_app_configuration() + + answer_question( + question={ + "data": {}, + "attributes": { + "question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", + "parent_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", + "originator_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", + "parent": "some/originator:service", + "originator": "some/originator:service", + "retry_count": 0, + }, + }, + project_name="a-project-name", + service_configuration=service_config, + app_configuration=app_config, + ) + + self.assertTrue( + mock_constructor.call_args.kwargs["service_configuration"].app_source_path.endswith("path/to/app_dir") + ) + + self.assertTrue( + mock_constructor.call_args.kwargs["service_configuration"].twine_path.endswith("path/to/twine.json") + ) + + self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].diagnostics_cloud_path) + self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].service_registries) + + self.assertEqual( + mock_constructor.call_args.kwargs["app_configuration"].configuration_values, {"hello": "configuration"} + ) + + self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].configuration_manifest) + self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].children) + self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].output_location) + + self.assertEqual(mock_constructor.call_args.kwargs["project_name"], "a-project-name") + self.assertEqual(mock_constructor.call_args.kwargs["service_id"], "testing/test-service:blah") diff --git a/tests/cloud/events/test_attributes.py b/tests/cloud/events/test_attributes.py new file mode 100644 index 000000000..d802e9cf7 --- /dev/null +++ b/tests/cloud/events/test_attributes.py @@ -0,0 +1,267 @@ +import unittest + +from octue.cloud.events.attributes import QuestionAttributes, ResponseAttributes + +QUESTION_UUID = "50760303-ee89-4752-81cc-aadd05f81752" +SENDER = "my-org/my-parent:1.0.0" +SENDER_TYPE = "PARENT" +RECIPIENT = "my-org/my-child:2.0.0" + + +class TestQuestionAttributes(unittest.TestCase): + def test_defaults(self): + """Test that the defaults are correct.""" + attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + attributes_dict = attributes.__dict__ + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + attributes_dict, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "parent_question_uuid": None, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + "forward_logs": None, + "save_diagnostics": None, + "cpus": None, + "memory": None, + "ephemeral_storage": None, + }, + ) + + def test_to_minimal_dict(self): + """Test that non-`None` attributes are excluded when making a minimal dictionary from attributes.""" + attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + attributes_dict = attributes.to_minimal_dict() + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + attributes_dict, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + }, + ) + + def test_to_serialised_attributes(self): + """Test that attributes are serialised correctly.""" + attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + forward_logs=True, + save_diagnostics="SAVE_DIAGNOSTICS_ON", + cpus=1, + memory="2Gi", + ephemeral_storage="256Mi", + ) + + serialised_attributes = attributes.to_serialised_attributes() + + self.assertTrue(serialised_attributes.pop("uuid")) + self.assertTrue(serialised_attributes.pop("sender_sdk_version")) + self.assertTrue(isinstance(serialised_attributes.pop("datetime"), str)) + + self.assertEqual( + serialised_attributes, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": "0", + "forward_logs": "1", + "save_diagnostics": "SAVE_DIAGNOSTICS_ON", + "cpus": "1", + "memory": "2Gi", + "ephemeral_storage": "256Mi", + }, + ) + + def test_reset_uuid_and_datetime(self): + """Test that the `reset_uuid_and_datetime` method changes the UUID and datetime.""" + attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + original_uuid = attributes.uuid + original_datetime = attributes.datetime + + attributes.reset_uuid_and_datetime() + self.assertNotEqual(attributes.uuid, original_uuid) + self.assertNotEqual(attributes.datetime, original_datetime) + + +class TestResponseAttributes(unittest.TestCase): + def test_defaults(self): + """Test that the defaults are correct.""" + attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + attributes_dict = attributes.__dict__ + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + attributes_dict, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "parent_question_uuid": None, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + "forward_logs": None, + "save_diagnostics": None, + "cpus": None, + "memory": None, + "ephemeral_storage": None, + }, + ) + + def test_to_minimal_dict(self): + """Test that non-`None` attributes are excluded when making a minimal dictionary from attributes.""" + attributes = ResponseAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + attributes_dict = attributes.to_minimal_dict() + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + attributes_dict, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + }, + ) + + def test_to_serialised_attributes(self): + """Test that attributes are serialised correctly.""" + attributes = ResponseAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + serialised_attributes = attributes.to_serialised_attributes() + + self.assertTrue(serialised_attributes.pop("uuid")) + self.assertTrue(serialised_attributes.pop("sender_sdk_version")) + self.assertTrue(isinstance(serialised_attributes.pop("datetime"), str)) + + self.assertEqual( + serialised_attributes, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": "0", + }, + ) + + def test_reset_uuid_and_datetime(self): + """Test that the `reset_uuid_and_datetime` method changes the UUID and datetime.""" + attributes = ResponseAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + original_uuid = attributes.uuid + original_datetime = attributes.datetime + + attributes.reset_uuid_and_datetime() + self.assertNotEqual(attributes.uuid, original_uuid) + self.assertNotEqual(attributes.datetime, original_datetime) + + def test_from_question_attributes(self): + """Test that the sender and recipient are reversed when making opposite attributes from a set of attributes.""" + question_attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + opposite_attributes = ResponseAttributes.from_question_attributes(question_attributes) + + opposite_attributes_dict = opposite_attributes.__dict__ + self.assertTrue(opposite_attributes_dict.pop("uuid")) + self.assertTrue(opposite_attributes_dict.pop("datetime")) + self.assertTrue(opposite_attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + opposite_attributes_dict, + { + "sender": RECIPIENT, + "sender_type": "CHILD", + "recipient": SENDER, + "question_uuid": QUESTION_UUID, + "parent_question_uuid": None, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + }, + ) diff --git a/tests/cloud/events/test_replayer.py b/tests/cloud/events/test_replayer.py index 0fae6e6ab..030fc8c01 100644 --- a/tests/cloud/events/test_replayer.py +++ b/tests/cloud/events/test_replayer.py @@ -7,11 +7,26 @@ from octue.cloud.events.replayer import EventReplayer from tests import TEST_BUCKET_NAME, TESTS_DIR - with open(os.path.join(TESTS_DIR, "data", "events.json")) as f: EVENTS = json.load(f) +ATTRIBUTES = { + "datetime": "2024-04-11T10:46:48.236064", + "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", + "retry_count": 0, + "question_uuid": "d45c7e99-d610-413b-8130-dd6eef46dda6", + "parent_question_uuid": "5776ad74-52a6-46f7-a526-90421d91b8b2", + "originator_question_uuid": "86dc55b2-4282-42bd-92d0-bd4991ae7356", + "parent": "octue/test-service:1.0.0", + "originator": "octue/test-service:1.0.0", + "sender": "octue/test-service:1.0.0", + "sender_type": "CHILD", + "sender_sdk_version": "0.51.0", + "recipient": "octue/another-service:3.2.1", +} + + EXPECTED_OUTPUT_MANIFEST = { "id": "a13713ae-f207-41c6-9e29-0a848ced6039", "name": None, @@ -31,42 +46,32 @@ class TestEventReplayer(unittest.TestCase): def test_with_no_events(self): """Test that `None` is returned if no events are passed in.""" - result = EventReplayer().handle_events(events=[]) + with self.assertLogs(level=logging.WARNING) as logging_context: + result = EventReplayer().handle_events(events=[]) + + self.assertIn("No result was found for this question.", logging_context.output[0]) self.assertIsNone(result) def test_with_no_valid_events(self): """Test that `None` is returned if no valid events are received.""" with self.assertLogs(level=logging.DEBUG) as logging_context: - result = EventReplayer(validate_events=True).handle_events(events=[{"invalid": "event"}]) + result = EventReplayer(validate_events=True).handle_events( + events=[{"invalid": "event", "attributes": ATTRIBUTES}] + ) self.assertIsNone(result) self.assertIn("received an event that doesn't conform", logging_context.output[1]) def test_no_result_event(self): """Test that `None` is returned if no result event is received.""" - event = { - "event": {"kind": "delivery_acknowledgement"}, - "attributes": { - "datetime": "2024-04-11T10:46:48.236064", - "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", - "retry_count": 0, - "question_uuid": "d45c7e99-d610-413b-8130-dd6eef46dda6", - "parent_question_uuid": "5776ad74-52a6-46f7-a526-90421d91b8b2", - "originator_question_uuid": "86dc55b2-4282-42bd-92d0-bd4991ae7356", - "parent": "octue/test-service:1.0.0", - "originator": "octue/test-service:1.0.0", - "sender": "octue/test-service:1.0.0", - "sender_type": "CHILD", - "sender_sdk_version": "0.51.0", - "recipient": "octue/another-service:3.2.1", - }, - } + event = {"event": {"kind": "delivery_acknowledgement"}, "attributes": ATTRIBUTES} with self.assertLogs() as logging_context: result = EventReplayer().handle_events(events=[event]) - self.assertIsNone(result) self.assertIn("question was delivered", logging_context.output[0]) + self.assertIn("No result was found for this question.", logging_context.output[1]) + self.assertIsNone(result) def test_with_events_including_result_event(self): """Test that stored events can be replayed and the outputs extracted from the "result" event.""" diff --git a/tests/cloud/pub_sub/test_bigquery.py b/tests/cloud/pub_sub/test_bigquery.py index 0a999fb5d..a60841639 100644 --- a/tests/cloud/pub_sub/test_bigquery.py +++ b/tests/cloud/pub_sub/test_bigquery.py @@ -1,3 +1,4 @@ +import logging from unittest import TestCase from unittest.mock import MagicMock, patch @@ -51,11 +52,13 @@ def test_error_raised_if_kinds_invalid(self): with self.assertRaises(ValueError): get_events(table_id="blah", question_uuid="blah", kinds=invalid_kinds) - def test_no_events_found(self): - """Test that an empty list is returned if no events are found for the question UUID.""" + def test_warning_logged_if_no_events_found(self): + """Test that an empty list is returned and a warning is logged if no events are found for the question UUID.""" with patch("octue.cloud.pub_sub.bigquery.Client", MockEmptyBigQueryClient): - events = get_events(table_id="blah", question_uuid="blah") + with self.assertLogs(level=logging.WARNING) as logging_context: + events = get_events(table_id="blah", question_uuid="blah") + self.assertIn("No events were found for this question.", logging_context.output[0]) self.assertEqual(events, []) def test_without_tail(self): diff --git a/tests/cloud/pub_sub/test_events.py b/tests/cloud/pub_sub/test_events.py index 614f58831..8431248a8 100644 --- a/tests/cloud/pub_sub/test_events.py +++ b/tests/cloud/pub_sub/test_events.py @@ -1,10 +1,11 @@ import datetime import os -import uuid from unittest.mock import patch +import uuid from octue.cloud.emulators._pub_sub import MESSAGES, MockMessage, MockService, MockSubscription from octue.cloud.emulators.service import ServicePatcher +from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler from octue.resources.service_backends import GCPPubSubBackend from tests import TEST_PROJECT_NAME @@ -30,6 +31,16 @@ def setUpClass(cls): backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME), ) + cls.attributes = ResponseAttributes( + question_uuid=cls.question_uuid, + originator_question_uuid=cls.question_uuid, + parent=cls.parent.id, + originator=cls.parent.id, + sender=cls.parent.id, + sender_type="CHILD", + recipient=cls.parent.id, + ) + @classmethod def tearDownClass(cls): """Stop the services patcher. @@ -68,36 +79,14 @@ def test_handle_events(self): child = MockService(backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME)) events = [ - { - "event": {"kind": "test", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "test", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "test", "order": 2}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "finish-test", "order": 3}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "test", "order": 0}}, + {"event": {"kind": "test", "order": 1}}, + {"event": {"kind": "test", "order": 2}}, + {"event": {"kind": "finish-test", "order": 3}}, ] for event in events: - child._emit_event( - event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, - ) + child._emit_event(event=event["event"], attributes=self.attributes) result = event_handler.handle_events() self.assertEqual(result, "This is the result.") @@ -126,32 +115,13 @@ def test_no_timeout(self): child = MockService(backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME)) events = [ - { - "event": {"kind": "test", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "test", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "finish-test", "order": 2}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "test", "order": 0}}, + {"event": {"kind": "test", "order": 1}}, + {"event": {"kind": "finish-test", "order": 2}}, ] for event in events: - child._emit_event( - event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, - ) + child._emit_event(event=event["event"], attributes=self.attributes) result = event_handler.handle_events(timeout=None) @@ -167,27 +137,14 @@ def test_delivery_acknowledgement(self): child = MockService(backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME)) events = [ - { - "event": {"kind": "delivery_acknowledgement", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "result", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "delivery_acknowledgement", "order": 0}}, + {"event": {"kind": "result", "order": 1}}, ] for event in events: child._emit_event( event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, + attributes=self.attributes, ) result = event_handler.handle_events() @@ -220,28 +177,12 @@ def test_error_not_raised_if_heartbeat_has_been_received_in_maximum_allowed_inte event_handler._last_heartbeat = datetime.datetime.now() events = [ - { - "event": {"kind": "delivery_acknowledgement", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "result", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "delivery_acknowledgement", "order": 0}}, + {"event": {"kind": "result", "order": 1}}, ] for event in events: - child._emit_event( - event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, - ) + child._emit_event(event=event["event"], attributes=self.attributes) with patch( "octue.cloud.pub_sub.events.GoogleCloudPubSubEventHandler._time_since_last_heartbeat", diff --git a/tests/cloud/pub_sub/test_logging.py b/tests/cloud/pub_sub/test_logging.py index 6e3ecfad7..5a2aefc65 100644 --- a/tests/cloud/pub_sub/test_logging.py +++ b/tests/cloud/pub_sub/test_logging.py @@ -5,10 +5,23 @@ from octue.cloud.emulators._pub_sub import MESSAGES, MockService from octue.cloud.emulators.service import ServicePatcher +from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.resources.service_backends import GCPPubSubBackend from tests.base import BaseTestCase +QUESTION_UUID = "96d69278-44ac-4631-aeea-c90fb08a1b2b" + +ATTRIBUTES = ResponseAttributes( + question_uuid=QUESTION_UUID, + originator_question_uuid=QUESTION_UUID, + parent="another/service:1.0.0", + originator="another/service:1.0.0", + sender="another/service:1.0.0", + sender_type="CHILD", + recipient="another/service:1.0.0", +) + class NonJSONSerialisable: def __repr__(self): @@ -36,23 +49,13 @@ def tearDownClass(cls): def test_emit(self): """Test the log message is published when `GoogleCloudPubSubHandler.emit` is called.""" - question_uuid = "96d69278-44ac-4631-aeea-c90fb08a1b2b" log_record = makeLogRecord({"msg": "Starting analysis."}) service = MockService(backend=GCPPubSubBackend(project_name="blah")) - GoogleCloudPubSubHandler( - event_emitter=service._emit_event, - question_uuid=question_uuid, - parent_question_uuid=None, - originator_question_uuid=question_uuid, - parent="another/service:1.0.0", - originator="another/service:1.0.0", - recipient="another/service:1.0.0", - retry_count=0, - ).emit(log_record) + GoogleCloudPubSubHandler(event_emitter=service._emit_event, attributes=ATTRIBUTES).emit(log_record) self.assertEqual( - json.loads(MESSAGES[question_uuid][0].data.decode())["log_record"]["msg"], + json.loads(MESSAGES[QUESTION_UUID][0].data.decode())["log_record"]["msg"], "Starting analysis.", ) @@ -60,7 +63,6 @@ def test_emit_with_non_json_serialisable_args(self): """Test that non-JSON-serialisable arguments to log messages are converted to their string representation before being serialised and published to the Pub/Sub topic. """ - question_uuid = "96d69278-44ac-4631-aeea-c90fb08a1b2b" non_json_serialisable_thing = NonJSONSerialisable() # Check that it can't be serialised to JSON. @@ -74,16 +76,7 @@ def test_emit_with_non_json_serialisable_args(self): service = MockService(backend=GCPPubSubBackend(project_name="blah")) with patch("octue.cloud.emulators._pub_sub.MockPublisher.publish") as mock_publish: - GoogleCloudPubSubHandler( - event_emitter=service._emit_event, - question_uuid=question_uuid, - parent_question_uuid=None, - originator_question_uuid=question_uuid, - parent="another/service:1.0.0", - originator="another/service:1.0.0", - recipient="another/service:1.0.0", - retry_count=0, - ).emit(record) + GoogleCloudPubSubHandler(event_emitter=service._emit_event, attributes=ATTRIBUTES).emit(record) self.assertEqual( json.loads(mock_publish.call_args.kwargs["data"].decode())["log_record"]["msg"], diff --git a/tests/cloud/pub_sub/test_service.py b/tests/cloud/pub_sub/test_service.py index 6e5e6a1b1..b84faa1ce 100644 --- a/tests/cloud/pub_sub/test_service.py +++ b/tests/cloud/pub_sub/test_service.py @@ -5,13 +5,11 @@ import random import tempfile import time -from unittest import mock from unittest.mock import patch import google.api_core.exceptions import requests -import twined.exceptions from octue import Runner, exceptions from octue.cloud.emulators._pub_sub import ( DifferentMockAnalysis, @@ -29,7 +27,7 @@ from octue.resources.service_backends import GCPPubSubBackend from tests import MOCK_SERVICE_REVISION_TAG, TEST_BUCKET_NAME, TEST_PROJECT_NAME from tests.base import BaseTestCase - +import twined.exceptions logger = logging.getLogger(__name__) @@ -122,13 +120,6 @@ def test_missing_services_topic_results_in_error(self): with self.assertRaises(exceptions.ServiceNotFound): service.services_topic - def test_error_raised_if_service_revision_not_found_when_not_using_service_registry(self): - """Test that an error is raised if a service revision isn't found when not using a service registry.""" - service = MockService(backend=BACKEND) - - with self.assertRaises(exceptions.ServiceNotFound): - service.ask("non/existent:service") - def test_ask_unregistered_service_revision_when_service_registries_specified_results_in_error(self): """Test that an error is raised if attempting to ask an unregistered service a question when service registries are being used. @@ -142,11 +133,12 @@ def test_ask_unregistered_service_revision_when_service_registries_specified_res mock_response.status_code = 404 with patch("requests.get", return_value=mock_response): - with self.assertRaises(exceptions.ServiceNotFound): - service.ask( - service_id=f"my-org/unregistered-service:{MOCK_SERVICE_REVISION_TAG}", - input_values=[1, 2, 3, 4], - ) + with patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token"): + with self.assertRaises(exceptions.ServiceNotFound): + service.ask( + service_id=f"my-org/unregistered-service:{MOCK_SERVICE_REVISION_TAG}", + input_values=[1, 2, 3, 4], + ) def test_ask_unregistered_service_with_no_revision_tag_when_service_registries_specified_results_in_error(self): """Test that an error is raised when attempting to ask a question to an unregistered service without including @@ -161,8 +153,9 @@ def test_ask_unregistered_service_with_no_revision_tag_when_service_registries_s mock_response.status_code = 404 with patch("requests.get", return_value=mock_response): - with self.assertRaises(exceptions.ServiceNotFound): - service.ask(service_id="my-org/unregistered-service", input_values=[1, 2, 3, 4]) + with patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token"): + with self.assertRaises(exceptions.ServiceNotFound): + service.ask(service_id="my-org/unregistered-service", input_values=[1, 2, 3, 4]) def test_ask_service_with_no_revision_tag_when_service_registries_not_specified_results_in_error(self): """Test that an error is raised when attempting to ask a question to a service without including a revision tag @@ -766,38 +759,6 @@ def run_function(*args, **kwargs): delta=datetime.timedelta(0.05), ) - def test_runtime_timeout_warning_logged_if_running_on_cloud_run(self): - """Test that a warning is logged when the runtime timeout warning time is reached if the service is running on - Cloud Run. - """ - - def run_function(*args, **kwargs): - time.sleep(0.3) - return MockAnalysis() - - child = MockService(backend=BACKEND, run_function=lambda *args, **kwargs: run_function()) - parent = MockService(backend=BACKEND, children={child.id: child}) - child.serve() - - # Trigger the heartbeat check straight away. - with patch( - "octue.cloud.emulators._pub_sub.MockService.answer", - functools.partial(child.answer, heartbeat_interval=0.1), - ): - with patch( - "octue.cloud.pub_sub.service.Service._send_heartbeat_and_check_runtime", - functools.partial(child._send_heartbeat_and_check_runtime, runtime_timeout_warning_time=0), - ): - with mock.patch.dict(os.environ, COMPUTE_PROVIDER="GOOGLE_CLOUD_RUN"): - with self.assertLogs(level=logging.WARNING) as logging_context: - subscription, _ = parent.ask(service_id=child.id, input_values={}) - parent.wait_for_answer(subscription) - - self.assertIn( - "This analysis will reach the maximum runtime and be stopped soon.", - logging_context.output[0], - ) - def test_send_monitor_messages_periodically(self): """Test that monitor messages are sent periodically if set up in the run function and that the periodic monitor message thread doesn't stop the result from being received (i.e. message sending is thread-safe). diff --git a/tests/cloud/pub_sub/test_subscription.py b/tests/cloud/pub_sub/test_subscription.py index 8c1be44fc..b9c2934cf 100644 --- a/tests/cloud/pub_sub/test_subscription.py +++ b/tests/cloud/pub_sub/test_subscription.py @@ -80,23 +80,6 @@ def test_create_pull_subscription(self): self.assertEqual(response._pb.retry_policy.minimum_backoff.seconds, 10) self.assertEqual(response._pb.retry_policy.maximum_backoff.seconds, 600) - def test_create_push_subscription(self): - """Test that creating a push subscription works properly.""" - project_name = os.environ["TEST_PROJECT_NAME"] - topic = Topic(name="my-topic", project_name=project_name) - subscription = Subscription(name="world", topic=topic, push_endpoint="https://example.com/endpoint") - - with patch("google.pubsub_v1.SubscriberClient.create_subscription", new=MockSubscriptionCreationResponse): - response = subscription.create(allow_existing=True) - - self.assertEqual(response._pb.ack_deadline_seconds, 600) - self.assertEqual(response._pb.expiration_policy.ttl.seconds, THIRTY_ONE_DAYS) - self.assertEqual(response._pb.message_retention_duration.seconds, 600) - self.assertTrue(response._pb.enable_message_ordering) - self.assertEqual(response._pb.retry_policy.minimum_backoff.seconds, 10) - self.assertEqual(response._pb.retry_policy.maximum_backoff.seconds, 600) - self.assertEqual(response._pb.push_config.push_endpoint, "https://example.com/endpoint") - def test_is_pull_subscription(self): """Test that `is_pull_subscription` is `True` for a pull subscription.""" self.assertTrue(self.subscription.is_pull_subscription) diff --git a/tests/cloud/test_registry.py b/tests/cloud/test_registry.py new file mode 100644 index 000000000..8e08989fd --- /dev/null +++ b/tests/cloud/test_registry.py @@ -0,0 +1,130 @@ +import json +import unittest +from unittest.mock import patch + +import requests + +from octue.cloud.registry import get_default_sruid, raise_if_revision_not_registered +import octue.exceptions + + +class TestGetDefaultSRUID(unittest.TestCase): + SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + + @classmethod + def setUpClass(cls): + cls.id_token_patch = patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token") + cls.id_token_patch.start() + + @classmethod + def tearDownClass(cls): + cls.id_token_patch.stop() + + def test_error_raised_if_request_fails(self): + """Test that an error is raised if the request to the service registry fails.""" + mock_response = requests.Response() + mock_response.status_code = 403 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(requests.HTTPError): + get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_error_raised_if_revision_not_found(self): + """Test that an error is raised if no revision is found for the service in the given registries.""" + mock_response = requests.Response() + mock_response.status_code = 404 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(octue.exceptions.ServiceNotFound): + get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_get_latest_sruid(self): + """Test that the latest SRUID for a service can be found.""" + mock_response = requests.Response() + mock_response.status_code = 200 + mock_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() + + with patch("requests.get", return_value=mock_response): + latest_sruid = get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES, + ) + + self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") + + def test_get_latest_sruid_when_not_in_first_registry(self): + """Test that the latest SRUID for a service can be found when the service isn't in the first registry.""" + mock_failure_response = requests.Response() + mock_failure_response.status_code = 404 + + mock_success_response = requests.Response() + mock_success_response.status_code = 200 + mock_success_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() + + with patch("requests.get", side_effect=[mock_failure_response, mock_success_response]): + latest_sruid = get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES + + [{"name": "Another Registry", "endpoint": "cats.com/services"}], + ) + + self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") + + +class TestRaiseIfRevisionNotRegistered(unittest.TestCase): + SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + + @classmethod + def setUpClass(cls): + cls.id_token_patch = patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token") + cls.id_token_patch.start() + + @classmethod + def tearDownClass(cls): + cls.id_token_patch.stop() + + def test_error_raised_if_request_fails(self): + """Test that an error is raised if the request to the service registry fails.""" + mock_response = requests.Response() + mock_response.status_code = 403 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(requests.HTTPError): + raise_if_revision_not_registered( + sruid="my-org/my-service:1.0.0", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_error_raised_if_revision_not_found(self): + """Test that an error is raised if no revision is found for the service in the given registries.""" + mock_response = requests.Response() + mock_response.status_code = 404 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(octue.exceptions.ServiceNotFound): + raise_if_revision_not_registered( + sruid="my-org/my-service:1.0.0", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_no_error_raised_if_service_revision_registered(self): + """Test that no error is raised if a revision is found for the service in the given registries.""" + mock_response = requests.Response() + mock_response.status_code = 200 + mock_response._content = json.dumps({"revision_tag": "1.0.0"}).encode() + + with patch("requests.get", return_value=mock_response): + raise_if_revision_not_registered( + sruid="my-org/my-service:1.0.0", + service_registries=self.SERVICE_REGISTRIES, + ) diff --git a/tests/cloud/test_service_id.py b/tests/cloud/test_service_id.py index 606e76dfa..95d4097d8 100644 --- a/tests/cloud/test_service_id.py +++ b/tests/cloud/test_service_id.py @@ -1,18 +1,14 @@ -import json import logging import os import unittest from unittest.mock import patch -import requests - -import octue.exceptions from octue.cloud.service_id import ( + DEFAULT_NAMESPACE, convert_service_id_to_pub_sub_form, - get_default_sruid, + create_sruid, get_sruid_from_pub_sub_resource_name, get_sruid_parts, - raise_if_revision_not_registered, split_service_id, validate_sruid, ) @@ -74,6 +70,34 @@ def test_with_revision_tag_environment_variable(self): self.assertEqual(revision_tag, "this-is-a-tag") +class TestCreateSRUID(unittest.TestCase): + def test_error_raised_for_invalid_arguments(self): + """Test that an error is raised when trying to create an SRUID from invalid components.""" + for namespace, name, revision_tag in ( + ("MY-NAMESPACE", "my-name", "my-tag"), + ("my-namespace", "MY-NAME", "my-tag"), + ("my-namespace", "my-name", "@"), + ): + with self.subTest(namespace=namespace, name=name, revision_tag=revision_tag): + with self.assertRaises(InvalidServiceID): + create_sruid(namespace, name, revision_tag) + + def test_default(self): + """Test that a valid SRUID is created when no arguments are provided and that a different SRUID is created each + time. + """ + sruid = create_sruid() + validate_sruid(sruid) + self.assertTrue(sruid.startswith(DEFAULT_NAMESPACE)) + self.assertNotEqual(sruid, create_sruid()) + + def test_with_arguments(self): + """Test that a valid SRUID is created from valid arguments.""" + sruid = create_sruid("my-namespace", "my-name", "my-tag") + validate_sruid(sruid) + self.assertEqual(sruid, "my-namespace/my-name:my-tag") + + class TestConvertServiceIDToPubSubForm(unittest.TestCase): def test_convert_service_id_to_pub_sub_form(self): """Test that service IDs containing organisations, revision tags, and the services namespace are all converted @@ -94,8 +118,8 @@ def test_convert_service_id_to_pub_sub_form(self): class TestGetSRUIDFromPubSubResourceName(unittest.TestCase): def test_get_sruid_from_pub_sub_resource_name(self): """Test that an SRUID can be extracted from a Pub/Sub resource name.""" - sruid = get_sruid_from_pub_sub_resource_name("octue.example-service-cloud-run.0-3-2") - self.assertEqual(sruid, "octue/example-service-cloud-run:0.3.2") + sruid = get_sruid_from_pub_sub_resource_name("octue.example-service.0-3-2") + self.assertEqual(sruid, "octue/example-service:0.3.2") class TestValidateSRUID(unittest.TestCase): @@ -117,7 +141,7 @@ def test_error_raised_if_service_id_invalid(self): "MY-ORG/my-service:1.9.4", "my-org/MY-SERVICE:1.9.4", "my-org/MY-SERVICE:@", - f"my-org/my-service:{'1'*129}", + f"my-org/my-service:{'1' * 129}", "/my-service", "/my-service:", ): @@ -165,7 +189,7 @@ def test_error_raised_if_sruid_components_invalid(self): ("MY-ORG", "my-service", "1.9.4"), ("my-org", "MY-SERVICE", "1.9.4"), ("my-org", "my-service", "@"), - ("my-org", "my-service", f"{'1'*129}"), + ("my-org", "my-service", f"{'1' * 129}"), ): with self.subTest(namespace=namespace, name=name, revision_tag=revision_tag): with self.assertRaises(InvalidServiceID): @@ -200,82 +224,3 @@ def test_split_service_id(self): self.assertEqual(namespace, "octue") self.assertEqual(name, "my-service") self.assertIsNone(revision_tag) - - -class TestGetLatestSRUID(unittest.TestCase): - SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] - - def test_error_raised_if_revision_not_found(self): - """Test that an error is raised if no revision is found for the service in the given registries.""" - mock_response = requests.Response() - mock_response.status_code = 404 - - with patch("requests.get", return_value=mock_response): - with self.assertRaises(octue.exceptions.ServiceNotFound): - get_default_sruid( - namespace="my-org", - name="my-service", - service_registries=self.SERVICE_REGISTRIES, - ) - - def test_get_latest_sruid(self): - """Test that the latest SRUID for a service can be found.""" - mock_response = requests.Response() - mock_response.status_code = 200 - mock_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() - - with patch("requests.get", return_value=mock_response): - latest_sruid = get_default_sruid( - namespace="my-org", - name="my-service", - service_registries=self.SERVICE_REGISTRIES, - ) - - self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") - - def test_get_latest_sruid_when_not_in_first_registry(self): - """Test that the latest SRUID for a service can be found when the service isn't in the first registry.""" - mock_failure_response = requests.Response() - mock_failure_response.status_code = 404 - - mock_success_response = requests.Response() - mock_success_response.status_code = 200 - mock_success_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() - - with patch("requests.get", side_effect=[mock_failure_response, mock_success_response]): - latest_sruid = get_default_sruid( - namespace="my-org", - name="my-service", - service_registries=self.SERVICE_REGISTRIES - + [{"name": "Another Registry", "endpoint": "cats.com/services"}], - ) - - self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") - - -class TestRaiseIfRevisionNotRegistered(unittest.TestCase): - SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] - - def test_error_raised_if_revision_not_found(self): - """Test that an error is raised if no revision is found for the service in the given registries.""" - mock_response = requests.Response() - mock_response.status_code = 404 - - with patch("requests.get", return_value=mock_response): - with self.assertRaises(octue.exceptions.ServiceNotFound): - raise_if_revision_not_registered( - sruid="my-org/my-service:1.0.0", - service_registries=self.SERVICE_REGISTRIES, - ) - - def test_no_error_raised_if_service_revision_registered(self): - """Test that no error is raised if a revision is found for the service in the given registries.""" - mock_response = requests.Response() - mock_response.status_code = 200 - mock_response._content = json.dumps({"revision_tag": "1.0.0"}).encode() - - with patch("requests.get", return_value=mock_response): - raise_if_revision_not_registered( - sruid="my-org/my-service:1.0.0", - service_registries=self.SERVICE_REGISTRIES, - ) diff --git a/tests/data/data_dir_with_no_manifests/input/values.json b/tests/data/data_dir_with_no_manifests/input/values.json deleted file mode 100644 index 58275dbd0..000000000 --- a/tests/data/data_dir_with_no_manifests/input/values.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "height": 3 -} diff --git a/tests/mixins/test_identifiable.py b/tests/mixins/test_identifiable.py index 32b6a9dab..a649dd51b 100644 --- a/tests/mixins/test_identifiable.py +++ b/tests/mixins/test_identifiable.py @@ -76,4 +76,7 @@ class Inherit(Identifiable): with self.assertRaises(AttributeError) as e: resource.id = "07d38e81-6b00-4079-901b-e250ea3c7773" - self.assertIn("can't set attribute", e.exception.args[0]) + # Make test work across python versions. + self.assertTrue( + ("can't set attribute" in e.exception.args[0]) or ("object has no setter" in e.exception.args[0]) + ) diff --git a/tests/test_cli.py b/tests/test_cli.py index e1ce04b09..799fbcc17 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,19 +9,23 @@ from octue.cli import octue_cli from octue.cloud import storage -from octue.cloud.emulators._pub_sub import MockService, MockSubscription, MockTopic +from octue.cloud.emulators._pub_sub import MockService from octue.cloud.emulators.service import ServicePatcher -from octue.cloud.events import OCTUE_SERVICES_PREFIX -from octue.cloud.pub_sub import Topic from octue.configuration import AppConfiguration, ServiceConfiguration -from octue.resources import Dataset +from octue.resources import Dataset, Manifest from octue.utils.patches import MultiPatcher from tests import MOCK_SERVICE_REVISION_TAG, TEST_BUCKET_NAME, TESTS_DIR from tests.base import BaseTestCase - TWINE_FILE_PATH = os.path.join(TESTS_DIR, "data", "twines", "valid_schema_twine.json") +MOCK_CONFIGURATIONS = ( + ServiceConfiguration(name="test-app", namespace="testing"), + AppConfiguration(configuration_values={"n_iterations": 5}), +) + +RESULT = {"output_values": {"some": "data"}} + class TestCLI(BaseTestCase): def test_version(self): @@ -38,128 +42,561 @@ def test_help(self): self.assertEqual(help_result.output, h_result.output) -class TestRunCommand(BaseTestCase): - MOCK_CONFIGURATIONS = ( - ServiceConfiguration( - name="test-app", - namespace="testing", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_module"), - twine_path=TWINE_FILE_PATH, - app_configuration_path="blah.json", - ), - AppConfiguration(configuration_values={"n_iterations": 5}), - ) - - def test_run(self): - """Test that the `run` CLI command runs the given service and outputs the output values.""" - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): +class TestQuestionAskRemoteCommand(BaseTestCase): + SRUID = "my-org/my-service:1.0.0" + QUESTION_UUID = "81f35b28-068b-4314-9eeb-e55e60d0fe8a" + + def test_with_input_values(self): + """Test that the `octue question ask remote` CLI command works with just input values.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "remote", + self.SRUID, + '--input-values={"height": 3}', + ], + ) + + mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) + self.assertIn(json.dumps(RESULT), result.output) + + def test_with_input_manifest(self): + """Test that the `octue question ask remote` CLI command works with just an input manifest.""" + input_manifest = self.create_valid_manifest() + + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "remote", + self.SRUID, + f"--input-manifest={input_manifest.serialise()}", + ], + ) + + self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) + self.assertIn(json.dumps(RESULT), result.output) + + def test_with_input_values_and_manifest(self): + """Test that the `octue question ask remote` CLI command works with input values and input manifest.""" + input_values = {"height": 3} + input_manifest = self.create_valid_manifest() + + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "remote", + self.SRUID, + f"--input-values={json.dumps(input_values)}", + f"--input-manifest={input_manifest.serialise()}", + ], + ) + + self.assertEqual(mock_ask.call_args.kwargs["input_values"], input_values) + self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) + self.assertIn(json.dumps(RESULT), result.output) + + def test_with_output_manifest(self): + """Test that the `octue question ask remote` CLI command returns output manifests in a useful form.""" + result = {"output_values": {"some": "data"}, "output_manifest": self.create_valid_manifest()} + + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(result, self.QUESTION_UUID)): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "remote", + self.SRUID, + f"--input-values={json.dumps({'height': 3})}", + ], + ) + + output = json.loads(result.output) + self.assertEqual(output["output_values"], {"some": "data"}) + self.assertEqual(len(output["output_manifest"]["datasets"]), 2) + + def test_asynchronous(self): + """Test that the `octue question ask remote` CLI command works with the `--asynchronous` option and returns the + question UUID. + """ + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "remote", + self.SRUID, + '--input-values={"height": 3}', + "--asynchronous", + ], + ) + + mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=True) + self.assertIn(self.QUESTION_UUID, result.output) + + def test_with_no_service_configuration(self): + """Test that the command works when no service configuration is provided.""" + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ - "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', + "question", + "ask", + "remote", + self.SRUID, + '--input-values={"height": 3}', ], ) - self.assertIn(json.dumps({"width": 3}), result.output) + mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) + self.assertIn(json.dumps(RESULT), result.output) + - def test_run_with_output_values_file(self): - """Test that the `run` CLI command runs the given service and stores the output values in a file if the `-o` - option is given. +class TestQuestionAskLocalCommand(BaseTestCase): + def test_with_input_values(self): + """Test that the `octue question ask local` CLI command works with just input values and sends an originator + question. """ - with tempfile.NamedTemporaryFile(delete=False) as temporary_file: - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: result = CliRunner().invoke( octue_cli, [ - "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', - "-o", - temporary_file.name, + "question", + "ask", + "local", + '--input-values={"height": 3}', ], ) - with open(temporary_file.name) as f: - self.assertEqual(json.load(f), {"width": 3}) + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event. + question = mock_answer_question_kwargs["question"] + self.assertEqual(question["event"], {"kind": "question", "input_values": {"height": 3}}) + + # Check question attributes. + self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) + # The question should be an originator question. + self.assertEqual(question["attributes"]["question_uuid"], question["attributes"]["originator_question_uuid"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["originator"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["sender"]) + self.assertNotIn("parent_question_uuid", question["attributes"]) + + # Check the result is in the output. + self.assertIn(json.dumps(RESULT), result.output) + + def test_with_input_manifest(self): + """Test that the `octue question ask local` CLI command works with just an input manifest and sends an + originator question. + """ + input_manifest = self.create_valid_manifest() + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + f"--input-manifest={input_manifest.serialise()}", + ], + ) - self.assertIn(json.dumps({"width": 3}), result.output) + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event. + question = mock_answer_question_kwargs["question"] + self.assertEqual(Manifest.deserialise(question["event"]["input_manifest"]).id, input_manifest.id) + + # Check question attributes. + self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) + # The question should be an originator question. + self.assertEqual(question["attributes"]["question_uuid"], question["attributes"]["originator_question_uuid"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["originator"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["sender"]) + self.assertNotIn("parent_question_uuid", question["attributes"]) + + # Check the result is in the output. + self.assertIn(json.dumps(RESULT), result.output) + + def test_with_input_values_and_manifest(self): + """Test that the `octue question ask local` CLI command works with input values and input manifest and sends an + originator question. + """ + input_values = {"height": 3} + input_manifest = self.create_valid_manifest() - def test_run_with_output_manifest(self): - """Test that the `run` CLI command runs the given service and stores the output manifest in a file.""" - with tempfile.NamedTemporaryFile("w", delete=False, suffix=".json") as temporary_twine: - temporary_twine.write( - json.dumps({"input_values_schema": {}, "output_manifest": {"datasets": {}}, "output_values_schema": {}}) - ) + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + f"--input-values={json.dumps(input_values)}", + f"--input-manifest={input_manifest.serialise()}", + ], + ) - mock_configurations = ( - ServiceConfiguration( - name="test-app", - namespace="testing", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_module_with_output_manifest"), - twine_path=temporary_twine.name, - ), - AppConfiguration(), - ) + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event. + question = mock_answer_question_kwargs["question"] + self.assertEqual(question["event"]["input_values"], input_values) + self.assertEqual(Manifest.deserialise(question["event"]["input_manifest"]).id, input_manifest.id) + + # Check question attributes. + self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) + # The question should be an originator question. + self.assertEqual(question["attributes"]["question_uuid"], question["attributes"]["originator_question_uuid"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["originator"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["sender"]) + self.assertNotIn("parent_question_uuid", question["attributes"]) + + # Check the result is in the output. + self.assertIn(json.dumps(RESULT), result.output) + + def test_with_output_manifest(self): + """Test that the `octue question ask local` CLI command returns output manifests in a useful form.""" + result = {"output_values": {"some": "data"}, "output_manifest": self.create_valid_manifest()} + + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=result): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + f"--input-values={json.dumps({'height': 3})}", + ], + ) + + output = json.loads(result.output) + self.assertEqual(output["output_values"], {"some": "data"}) + self.assertEqual(len(output["output_manifest"]["datasets"]), 2) - with tempfile.NamedTemporaryFile(delete=False) as temporary_manifest: - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=mock_configurations): + def test_with_attributes(self): + """Test that the `octue question ask remote` CLI command can be passed question attributes which are passed + along to the answering `Service` instance. + """ + original_attributes = { + "datetime": "2024-04-11T10:46:48.236064", + "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", + "question_uuid": "d45c7e99-d610-413b-8130-dd6eef46dda6", + "parent_question_uuid": "5776ad74-52a6-46f7-a526-90421d91b8b2", + "originator_question_uuid": "86dc55b2-4282-42bd-92d0-bd4991ae7356", + "parent": "octue/the-parent:1.0.0", + "originator": "octue/the-originator:1.0.5", + "sender": "octue/the-sender:2.0.0", + "sender_type": "CHILD", + "sender_sdk_version": "0.51.0", + "recipient": "octue/the-recipient:0.3.2", + "retry_count": 1, + "forward_logs": True, + "save_diagnostics": "SAVE_DIAGNOSTICS_OFF", + "cpus": 3, + "memory": "10Gi", + "ephemeral_storage": "500Mi", + } + + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: result = CliRunner().invoke( octue_cli, [ - "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', - f"--output-manifest-file={temporary_manifest.name}", + "question", + "ask", + "local", + '--input-values={"height": 3}', + f"--attributes={json.dumps(original_attributes)}", ], ) - with open(temporary_manifest.name) as f: - self.assertIn("datasets", json.load(f)) - - self.assertIn(json.dumps({"width": 3}), result.output) - - def test_run_with_monitor_messages_sent_to_file(self): - """Test that, when the `--monitor-messages-file` is provided, any monitor messages are written to it.""" - mock_configurations = ( - ServiceConfiguration( - name="test-app", - namespace="testing", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_with_monitor_message"), - twine_path=TWINE_FILE_PATH, - app_configuration_path="blah.json", - ), - AppConfiguration(configuration_values={"n_iterations": 5}), - ) + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event and attributes. + question = mock_answer_question_kwargs["question"] + self.assertEqual(question["event"], {"kind": "question", "input_values": {"height": 3}}) + self.assertEqual(question["attributes"], original_attributes) + + # Check the result is in the output. + self.assertIn(json.dumps(RESULT), result.output) + + +class TestQuestionEventsGetCommand(BaseTestCase): + QUESTION_UUID = "3ffc192c-7db0-4941-9b66-328a9fc02b62" + + with open(os.path.join(TESTS_DIR, "data", "events.json")) as f: + EVENTS = json.load(f) + + def test_warning_logged_if_no_events_found(self): + """Test that a warning is logged if no events are found for a question.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=[]): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + ], + ) + + self.assertTrue(result.output.endswith("[]\n")) + + def test_with_question_uuid_types(self): + """Test that each of the question UUID types is valid as an argument by themselves.""" + for question_uuid_arg in ( + "--question-uuid", + "--parent-question-uuid", + "--originator-question-uuid", + ): + with self.subTest(question_uuid_arg=question_uuid_arg): + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + question_uuid_arg, + self.QUESTION_UUID, + ], + ) + + self.assertEqual(json.loads(result.output), self.EVENTS) + + def test_with_kinds(self): + """Test that the `--kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[-2:-1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + "--kinds", + "result", + ], + ) + + self.assertEqual(mock_get_events.call_args.kwargs["kinds"], ["result"]) + self.assertIsNone(mock_get_events.call_args.kwargs["exclude_kinds"]) + self.assertEqual(json.loads(result.output), self.EVENTS[-2:-1]) + + def test_with_exclude_kinds(self): + """Test that the `--exclude-kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + "--exclude-kinds", + "log_record,result,heartbeat", + ], + ) + + self.assertIsNone(mock_get_events.call_args.kwargs["kinds"]) + self.assertEqual(mock_get_events.call_args.kwargs["exclude_kinds"], ["log_record", "result", "heartbeat"]) + self.assertEqual(json.loads(result.output), self.EVENTS[:1]) + + def test_with_limit(self): + """Test that the `--limit` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + "--limit", + "1", + ], + ) + + self.assertEqual(mock_get_events.call_args.kwargs["limit"], 1) + self.assertEqual(json.loads(result.output), self.EVENTS[:1]) + + +class TestQuestionEventsReplayCommand(BaseTestCase): + QUESTION_UUID = "3ffc192c-7db0-4941-9b66-328a9fc02b62" + + with open(os.path.join(TESTS_DIR, "data", "events.json")) as f: + EVENTS = json.load(f) - with tempfile.NamedTemporaryFile(delete=False) as monitor_messages_file: - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=mock_configurations): + maxDiff = None + + def test_with_no_events_found(self): + """Test that an empty list is passed to `stdout` if no events are found for a question.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=[]): result = CliRunner().invoke( octue_cli, [ - "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', - f"--monitor-messages-file={monitor_messages_file.name}", + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, ], ) - with open(monitor_messages_file.name) as f: - self.assertEqual(json.load(f), [{"status": "hello"}]) + self.assertEqual(result.output, "") + + def test_with_question_uuid_types_and_validate_events(self): + """Test that each of the question UUID types is valid as an argument by themselves and that replaying still + works with the `--validate-events` option. + """ + for options in ( + ["--question-uuid"], + ["--parent-question-uuid"], + ["--originator-question-uuid"], + ["--validate-events", "--question-uuid"], + ): + with self.subTest(question_uuid_arg=options): + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS): + with self.assertLogs() as logging_context: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + *options, + self.QUESTION_UUID, + ], + ) + + replayed_log_messages = "\n".join(logging_context.output) + log_record_events = [event for event in self.EVENTS if event["event"]["kind"] == "log_record"] + + # Check logs messages were replayed. + for event in log_record_events: + self.assertIn(event["event"]["log_record"]["msg"], replayed_log_messages) + + # Check result is correct. + self.assertEqual( + json.loads(result.output.splitlines()[-1])["output_values"], + self.EVENTS[-2]["event"]["output_values"], + ) + + def test_with_kinds(self): + """Test that the `--kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[-2:-1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, + "--kinds", + "result", + ], + ) + + # Check result is correct. + self.assertEqual( + json.loads(result.output.splitlines()[-1])["output_values"], + self.EVENTS[-2]["event"]["output_values"], + ) + + self.assertEqual(mock_get_events.call_args.kwargs["kinds"], ["result"]) + self.assertIsNone(mock_get_events.call_args.kwargs["exclude_kinds"]) - self.assertIn(json.dumps({"width": 3}), result.output) + def test_with_exclude_kinds(self): + """Test that the `--exclude-kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, + "--exclude-kinds", + "log_record,result,heartbeat", + ], + ) + + # Check result isn't replayed. + self.assertTrue(result.output.splitlines()[-1].endswith("No result was found for this question.")) + self.assertIsNone(mock_get_events.call_args.kwargs["kinds"]) + self.assertEqual(mock_get_events.call_args.kwargs["exclude_kinds"], ["log_record", "result", "heartbeat"]) - def test_remote_logger_uri_can_be_set(self): - """Test that remote logger URI can be set via the CLI and that this is logged locally.""" - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): - with mock.patch("logging.StreamHandler.emit") as mock_local_logger_emit: - CliRunner().invoke( + def test_with_limit(self): + """Test that the `--limit` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( octue_cli, [ - "--logger-uri=wss://0.0.0.1:3000", - "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, + "--limit", + "1", ], ) - mock_local_logger_emit.assert_called() + # Check result isn't replayed. + self.assertTrue(result.output.splitlines()[-1].endswith("No result was found for this question.")) + self.assertEqual(mock_get_events.call_args.kwargs["limit"], 1) class TestStartCommand(BaseTestCase): @@ -247,13 +684,13 @@ def test_start_command_with_revision_tag_override_when_revision_tag_environment_ self.assertEqual(result.exit_code, 0) -class TestGetDiagnosticsCommand(BaseTestCase): +class TestQuestionDiagnosticsCommand(BaseTestCase): DIAGNOSTICS_CLOUD_PATH = storage.path.generate_gs_path(TEST_BUCKET_NAME, "diagnostics") ANALYSIS_ID = "dc1f09ca-7037-484f-a394-8bd04866f924" @classmethod def setUpClass(cls): - """Upload the test diagnostics data to the cloud storage emulator so the `octue get-diagnostics` CLI command can + """Upload the test diagnostics data to the cloud storage emulator so the `octue question diagnostics` CLI command can be tested. :return None: @@ -268,13 +705,33 @@ def setUpClass(cls): diagnostics.upload(storage.path.join(cls.DIAGNOSTICS_CLOUD_PATH, cls.ANALYSIS_ID)) + def test_old_get_diagnostics_command_deprecated(self): + """Test that the old `octue get-diagnostics` command is deprecated and just calls the new one.""" + cloud_path = storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, self.ANALYSIS_ID) + local_path = "some-local-path" + + with patch("octue.cli.diagnostics") as mock_diagnostics: + result = CliRunner().invoke( + octue_cli, + [ + "get-diagnostics", + cloud_path, + "--local-path", + local_path, + ], + ) + + mock_diagnostics.assert_called_with(cloud_path, local_path, False) + self.assertEqual(result.output, "DeprecationWarning: The command 'get-diagnostics' is deprecated.\n") + def test_warning_logged_if_no_diagnostics_found(self): """Test that a warning about there being no diagnostics is logged if the diagnostics cloud path is empty.""" with tempfile.TemporaryDirectory() as temporary_directory: result = CliRunner().invoke( octue_cli, [ - "get-diagnostics", + "question", + "diagnostics", storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, "9f4ccee3-15b0-4a03-b5ac-c19e1d66a709"), "--local-path", temporary_directory, @@ -297,13 +754,14 @@ def test_warning_logged_if_no_diagnostics_found(self): def test_get_diagnostics(self): """Test that only the values files, manifests, and questions file are downloaded when using the - `get-diagnostics` CLI command. + `question diagnostics` CLI command. """ with tempfile.TemporaryDirectory() as temporary_directory: result = CliRunner().invoke( octue_cli, [ - "get-diagnostics", + "question", + "diagnostics", storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, self.ANALYSIS_ID), "--local-path", temporary_directory, @@ -349,13 +807,14 @@ def test_get_diagnostics(self): def test_get_diagnostics_with_datasets(self): """Test that datasets are downloaded as well as the values files, manifests, and questions file when the - `get-diagnostics` CLI command is run with the `--download-datasets` flag. + `question diagnostics` CLI command is run with the `--download-datasets` flag. """ with tempfile.TemporaryDirectory() as temporary_directory: result = CliRunner().invoke( octue_cli, [ - "get-diagnostics", + "question", + "diagnostics", storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, self.ANALYSIS_ID), "--local-path", temporary_directory, @@ -413,75 +872,3 @@ def test_get_diagnostics_with_datasets(self): {"kind": "result", "output_values": [1, 2, 3, 4, 5]}, ], ) - - -class TestDeployCommand(BaseTestCase): - def test_deploy_command_group(self): - """Test that the `create-push-subscription` command is a subcommand of the `deploy` command.""" - result = CliRunner().invoke(octue_cli, ["deploy", "--help"]) - self.assertIn("create-push-subscription ", result.output) - - def test_create_push_subscription(self): - """Test that a push subscription can be created using the `octue deploy create-push-subscription` command and - that its expiry time is correct. - """ - for expiration_time_option, expected_expiration_time in ( - ([], None), - (["--expiration-time="], None), - (["--expiration-time=100"], 100), - ): - with self.subTest(expiration_time_option=expiration_time_option): - with patch("octue.cloud.pub_sub.Topic", new=MockTopic): - with patch("octue.cloud.pub_sub.Subscription") as subscription: - result = CliRunner().invoke( - octue_cli, - [ - "deploy", - "create-push-subscription", - "my-project", - "octue", - "example-service", - "https://example.com/endpoint", - *expiration_time_option, - "--revision-tag=3.5.0", - ], - ) - - self.assertIsNone(result.exception) - self.assertEqual(result.exit_code, 0) - self.assertEqual(subscription.call_args.kwargs["name"], "octue.example-service.3-5-0") - self.assertEqual(subscription.call_args.kwargs["push_endpoint"], "https://example.com/endpoint") - self.assertEqual(subscription.call_args.kwargs["expiration_time"], expected_expiration_time) - self.assertEqual(result.output, "Subscription for 'octue/example-service:3.5.0' created.\n") - - def test_create_push_subscription_when_already_exists(self): - """Test attempting to create a push subscription for a service revision when one already exists for it.""" - sruid = "octue.example-service.3-5-0" - push_endpoint = "https://example.com/endpoint" - - with patch("octue.cloud.pub_sub.Topic", new=MockTopic): - with patch("octue.cloud.pub_sub.Subscription", new=MockSubscription): - subscription = MockSubscription( - name=sruid, - topic=Topic(name=OCTUE_SERVICES_PREFIX, project_name="my-project"), - push_endpoint=push_endpoint, - ) - - subscription.create() - - result = CliRunner().invoke( - octue_cli, - [ - "deploy", - "create-push-subscription", - "my-project", - "octue", - "example-service", - push_endpoint, - "--revision-tag=3.5.0", - ], - ) - - self.assertIsNone(result.exception) - self.assertEqual(result.exit_code, 0) - self.assertEqual(result.output, "Subscription for 'octue/example-service:3.5.0' already exists.\n") diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 241f904c6..b6477d1cf 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -64,21 +64,21 @@ class TestWarnIfIncompatible(BaseTestCase): def test_warn_if_incompatible_with_missing_child_version_information(self): """Test that a warning is raised when calling `warn_if_incompatible` with missing child version information.""" with self.assertLogs(level=logging.WARNING) as logging_context: - warn_if_incompatible(parent_sdk_version="0.51.0", child_sdk_version=None) + warn_if_incompatible(sender_sdk_version="0.51.0", recipient_sdk_version=None) self.assertIn( - "The child couldn't be checked for compatibility with this service because its Octue SDK version wasn't " - "provided. Please update it to the latest Octue SDK version.", + "The recipient couldn't be checked for compatibility with this service because its Octue SDK version " + "wasn't provided. Please update it to the latest Octue SDK version.", logging_context.output[0], ) def test_warn_if_incompatible_with_missing_parent_version_information(self): """Test that a warning is raised when calling `warn_if_incompatible` with missing parent version information.""" with self.assertLogs(level=logging.WARNING) as logging_context: - warn_if_incompatible(parent_sdk_version=None, child_sdk_version="0.16.0") + warn_if_incompatible(sender_sdk_version=None, recipient_sdk_version="0.16.0") self.assertIn( - "The parent couldn't be checked for compatibility with this service because its Octue SDK version wasn't " + "The sender couldn't be checked for compatibility with this service because its Octue SDK version wasn't " "provided. Please update it to the latest Octue SDK version.", logging_context.output[0], ) @@ -86,10 +86,10 @@ def test_warn_if_incompatible_with_missing_parent_version_information(self): def test_warn_if_incompatible_with_incompatible_versions(self): """Test that a warning is raised if incompatible versions are detected.""" with self.assertLogs(level=logging.WARNING) as logging_context: - warn_if_incompatible(parent_sdk_version="0.50.0", child_sdk_version="0.51.0") + warn_if_incompatible(sender_sdk_version="0.50.0", recipient_sdk_version="0.51.0") self.assertIn( - "The parent's Octue SDK version 0.50.0 is incompatible with the child's version 0.51.0. Please update " + "The sender's Octue SDK version 0.50.0 is incompatible with the recipient's version 0.51.0. Please update " "either or both to the latest version.", logging_context.output[0], ) @@ -100,7 +100,7 @@ def test_warn_if_incompatible_with_compatible_versions(self): try: with self.assertLogs(level=logging.WARNING): - warn_if_incompatible(parent_sdk_version="0.40.0", child_sdk_version="0.40.0") + warn_if_incompatible(sender_sdk_version="0.40.0", recipient_sdk_version="0.40.0") except AssertionError: no_warnings = True diff --git a/tests/test_log_handlers.py b/tests/test_log_handlers.py index d54fc69a9..ff0902d45 100644 --- a/tests/test_log_handlers.py +++ b/tests/test_log_handlers.py @@ -17,11 +17,11 @@ class TestLogging(BaseTestCase): - def test_log_record_attributes_without_timestamp_used_if_compute_provider_is_google_cloud_run(self): + def test_log_record_attributes_without_timestamp_used_if_compute_provider_is_google_cloud_function(self): """Test that the formatter without a timestamp is used for logging if the `COMPUTE_PROVIDER` environment - variable is present and equal to "GOOGLE_CLOUD_RUN", and `USE_OCTUE_LOG_HANDLER` is equal to "1". + variable is present and equal to "GOOGLE_CLOUD_FUNCTION", and `USE_OCTUE_LOG_HANDLER` is equal to "1". """ - with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="GOOGLE_CLOUD_RUN"): + with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="GOOGLE_CLOUD_FUNCTION"): with mock.patch("octue.log_handlers.create_octue_formatter") as create_octue_formatter: importlib.reload(sys.modules["octue"]) @@ -32,9 +32,9 @@ def test_log_record_attributes_without_timestamp_used_if_compute_provider_is_goo include_thread_name=False, ) - def test_log_record_attributes_with_timestamp_used_if_compute_provider_is_not_google_cloud_run(self): + def test_log_record_attributes_with_timestamp_used_if_compute_provider_is_not_google_cloud_function(self): """Test that the formatter without a timestamp is used for logging if the `COMPUTE_PROVIDER` environment - variable is present and not equal to "GOOGLE_CLOUD_RUN", and `USE_OCTUE_LOG_HANDLER` is equal to "1". + variable is present and not equal to "GOOGLE_CLOUD_FUNCTION", and `USE_OCTUE_LOG_HANDLER` is equal to "1". """ with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="BLAH"): with mock.patch("octue.log_handlers.create_octue_formatter") as create_octue_formatter: