From e67f115b54b14f3c82b9f5ef2911c399e31fee99 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 10:15:00 +0800 Subject: [PATCH 01/14] add new env setup instructions --- .../create-a-minimal-kubernetes-charm.md | 6 +- .../integrate-your-charm-with-postgresql.md | 4 +- .../observe-your-charm-with-cos-lite.md | 8 +- .../set-up-your-development-environment.md | 136 +++++++++++++++--- .../write-your-first-machine-charm.md | 3 +- 5 files changed, 131 insertions(+), 26 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 552bde2ce..6fad13ae5 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -303,7 +303,7 @@ Once you've mastered the basics, you can speed things up by navigating to your e First, ensure that you are inside the Multipass Ubuntu VM, in the `~/fastapi-demo` folder: ``` -multipass shell charm-dev +multipass shell juju-sandbox-k8s cd ~/fastapi-demo ``` @@ -326,7 +326,7 @@ If packing failed - perhaps you forgot to make `charm.py` executable earlier - y ``` - + @@ -536,7 +536,7 @@ def test_pebble_layer(): In your Multipass Ubuntu VM shell, run your test: ```text -ubuntu@charm-dev:~/fastapi-demo$ tox -e unit +ubuntu@juju-sandbox-k8s:~/fastapi-demo$ tox -e unit ``` The result should be similar to the following output: diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 9dbb1d1dc..04cca1af1 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -39,7 +39,7 @@ This tells Charmcraft that your charm requires the [`data_interfaces`](https://c Next, run the following command to download the library: ```text -ubuntu@charm-dev:~/fastapi-demo$ charmcraft fetch-libs +ubuntu@juju-sandbox-k8s:~/fastapi-demo$ charmcraft fetch-libs ``` Your charm directory should now contain the structure below: @@ -536,7 +536,7 @@ def test_database_integration(juju: jubilant.Juju): In your Multipass Ubuntu VM, run the test again: ```text -ubuntu@charm-dev:~/fastapi-demo$ tox -e integration +ubuntu@juju-sandbox-k8s:~/fastapi-demo$ tox -e integration ``` The test may again take some time to run. diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md index 38bfb91fe..a083152ff 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md @@ -48,7 +48,7 @@ charm-libs: Next, run the following command to download the libraries: ```text -ubuntu@charm-dev:~/fastapi-demo$ charmcraft fetch-libs +ubuntu@juju-sandbox-k8s:~/fastapi-demo$ charmcraft fetch-libs ``` Your charm directory should now include the structure below: @@ -333,13 +333,13 @@ Do not mix up Apps and Units -- Units represent Kubernetes pods while Apps repre Now open a terminal on your host machine and run: ```text -multipass info charm-dev +multipass info juju-sandbox-k8s ``` This should result in an output similar to the one below: ```text -Name: charm-dev +Name: juju-sandbox-k8s State: Running IPv4: 10.112.13.157 10.49.132.1 @@ -349,7 +349,7 @@ Image hash: 1d24e397489d (Ubuntu 22.04 LTS) Load: 0.31 0.25 0.28 Disk usage: 15.9G out of 19.2G Memory usage: 2.1G out of 7.8G -Mounts: /home/maksim/fastapi-demo => ~/fastapi-demo +Mounts: /home/maksim/k8s-tutorial => ~/fastapi-demo UID map: 1000:default GID map: 1000:default ``` diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/set-up-your-development-environment.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/set-up-your-development-environment.md index 59cf7195b..ab9d93930 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/set-up-your-development-environment.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/set-up-your-development-environment.md @@ -5,22 +5,126 @@ > > **See previous: {ref}`Study your application `** -In this chapter of the tutorial you will set up your development environment. - -You will need a charm directory, the various tools in the charm SDK, Juju, and a Kubernetes cloud. And it’s a good idea if you can do all your work in an isolated development environment. - -To set all of this up, see {external+juju:ref}`Juju | Manage your deployment > Set up your deployment - local testing and development `, with the following changes: - -- At the directory step, call your directory `fastapi-demo`. -- At the VM setup step, call your VM `charm-dev`. Also set up Docker: - ```text - sudo addgroup --system docker - sudo adduser $USER docker - newgrp docker - sudo snap install docker - ``` -- At the cloud selection step, choose `microk8s`. -- At the mount step, read the tips about how to edit files locally while running them inside the VM. +## Create a virtual machine + +You'll deploy and test your charm inside an Ubuntu virtual machine that's running on your computer. Your virtual machine will provide an isolated environment that's safe for you to experiment in, without affecting your host machine. This is especially helpful for the charm's integration tests, which require a local Juju controller and Kubernetes cloud. + +First, install Multipass for managing virtual machines. See the [installation instructions](https://canonical.com/multipass/install). + +Next, open a terminal, then run: + +```text +multipass launch --cpus 4 --memory 8G --disk 50G --name juju-sandbox-k8s +``` + +This creates a virtual machine called `juju-sandbox-k8s`. + +Multipass allocates some of your computer's memory and disk space to your virtual machine. The options we've chosen for `multipass launch` ensure that your virtual machine will be powerful enough to run Juju and deploy medium-sized charms. + +This step should take less than 10 minutes, but the time depends on your computer and network. When your virtual machine has been created, you'll see the message: + +```text +Launched: juju-sandbox-k8s +``` + +Now run: + +```text +multipass shell juju-sandbox-k8s +``` + +This switches the terminal so that you're working inside your virtual machine. + +You'll see a message with information about your virtual machine. You'll also see a new prompt: + +```text +ubuntu@juju-sandbox-k8s:~$ +``` + +## Install Juju and charm development tools + +Now that you have a virtual machine, you need to install the following tools on your virtual machine: + +- **Charmcraft, Juju, and MicroK8s** - You'll use {external+charmcraft:doc}`Charmcraft ` to create the initial version of your charm and prepare your charm for deployment. When you deploy your charm, Juju will use MicroK8s to create a Kubernetes cloud for your charm. +- **uv** - Your charm will be a Python project. You'll use [uv](https://docs.astral.sh/uv/) to manage your charm's runtime and development dependencies. +- **tox** - You'll use [tox](https://tox.wiki/en/) to run your charm's checks and tests. + +Instead of manually installing and configuring each tool, we recommend using [Concierge](https://github.com/canonical/concierge), Canonical's tool for setting up charm development environments. + +In your virtual machine, run: + +```text +sudo snap install --classic concierge +sudo concierge prepare -p microk8s --extra-snaps astral-uv +``` + +This first installs Concierge, then uses Concierge to install and configure the other tools (except tox). The option `-p microk8s` tells Concierge that we want tools for developing Kubernetes charms, with a local cloud managed by MicroK8s. + +This step should take less than 15 minutes, but the time depends on your computer and network. When the tools have been installed, you'll see a message that ends with: + +```text +msg="Bootstrapped Juju" provider=microk8s +``` + +To install tox, run: + +```text +uv tool install tox --with tox-uv +``` + +When tox has been installed, you'll see a confirmation and a warning: + +```text +Installed 1 executable: tox +warning: `/home/ubuntu/.local/bin` is not on your PATH. To use installed tools, +run `export PATH="/home/ubuntu/.local/bin:$PATH"` or `uv tool update-shell`. +``` + +Instead of following the warning, exit your virtual machine: + +```text +exit +``` + +The terminal switches back to your host machine. Your virtual machine is still running. + +Next, stop your virtual machine: + +```text +multipass stop juju-sandbox-k8s +``` + +Then use the Multipass {external+multipass:ref}`snapshot ` command to take a snapshot of your virtual machine: + +```text +multipass snapshot juju-sandbox-k8s +``` + +If you have any problems with your virtual machine during or after completing the tutorial, use the Multipass {external+multipass:ref}`restore ` command to restore your virtual machine to this point. + +## Create a project directory + +Although you'll deploy and test your charm inside your virtual machine, you'll probably find it more convenient to write your charm using your usual text editor or IDE. + +Outside your virtual machine, create a project directory: + +```text +mkdir ~/k8s-tutorial +``` + +You'll write your charm in this directory. + +Next, use the Multipass {external+multipass:ref}`mount ` command to make the directory available inside your virtual machine: + +```text +multipass mount --type native ~/k8s-tutorial juju-sandbox-k8s:~/fastapi-demo +``` + +Finally, start your virtual machine and switch to your virtual machine: + +```text +multipass shell juju-sandbox-k8s +``` Congratulations, your development environment is ready! diff --git a/docs/tutorial/write-your-first-machine-charm.md b/docs/tutorial/write-your-first-machine-charm.md index 7a3fd31da..5fc68572e 100644 --- a/docs/tutorial/write-your-first-machine-charm.md +++ b/docs/tutorial/write-your-first-machine-charm.md @@ -81,7 +81,8 @@ ubuntu@juju-sandbox:~$ Now that you have a virtual machine, you need to install the following tools on your virtual machine: - **Charmcraft, Juju, and LXD** - You'll use {external+charmcraft:doc}`Charmcraft ` to create the initial version of your charm and prepare your charm for deployment. When you deploy your charm, Juju will use LXD to manage the machine where your charm runs. -- **uv and tox** - You'll implement your charm using Python code. [uv](https://docs.astral.sh/uv/) is a Python project manager that will install dependencies for checks and tests. You'll use [tox](https://tox.wiki/en/) to select which checks or tests to run. +- **uv** - Your charm will be a Python project. You'll use [uv](https://docs.astral.sh/uv/) to manage your charm's runtime and development dependencies. +- **tox** - You'll use [tox](https://tox.wiki/en/) to run your charm's checks and tests. Instead of manually installing and configuring each tool, we recommend using [Concierge](https://github.com/canonical/concierge), Canonical's tool for setting up charm development environments. From dc7a0a8ffef16a0cfbea42b58c9ae26c449ca7c8 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 10:32:07 +0800 Subject: [PATCH 02/14] mention project dir before creating charmcraft.yaml --- .../create-a-minimal-kubernetes-charm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 6fad13ae5..18ab35b64 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -47,7 +47,7 @@ As a charm developer, your first job is to use this knowledge to create the basi ## Set the basic information, requirements, and workload for your charm -Create a file called `charmcraft.yaml`. This is a file that describes metadata such as the charm name, purpose, environment constraints, workload containers, etc., in short, all the information that tells Juju what it can do with your charm. +In your virtual machine, go into your project directory `~/fastapi-demo`, then create a file called `charmcraft.yaml`. This is a file that describes metadata such as the charm name, purpose, environment constraints, workload containers, etc., in short, all the information that tells Juju what it can do with your charm. In this file, do all of the following: From 007685d09f4b57b41dbaffba1c959eec5a102ddf Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 10:44:14 +0800 Subject: [PATCH 03/14] replace maksim by placeholder user --- .../observe-your-charm-with-cos-lite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md index a083152ff..6f2deffb6 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md @@ -349,7 +349,7 @@ Image hash: 1d24e397489d (Ubuntu 22.04 LTS) Load: 0.31 0.25 0.28 Disk usage: 15.9G out of 19.2G Memory usage: 2.1G out of 7.8G -Mounts: /home/maksim/k8s-tutorial => ~/fastapi-demo +Mounts: /home/me/k8s-tutorial => ~/fastapi-demo UID map: 1000:default GID map: 1000:default ``` From 46ea510d1f3c27b8426011524fe132f59690c1b7 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 15:07:02 +0800 Subject: [PATCH 04/14] update instructions for creating minimal charm --- .../create-a-minimal-kubernetes-charm.md | 213 +++++++----------- .../expose-operational-tasks-via-actions.md | 12 +- .../integrate-your-charm-with-postgresql.md | 20 +- .../make-your-charm-configurable.md | 16 +- .../observe-your-charm-with-cos-lite.md | 10 +- 5 files changed, 106 insertions(+), 165 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 552bde2ce..3675f089f 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -44,87 +44,63 @@ As a charm developer, your first job is to use this knowledge to create the basi - descriptive files (e.g., YAML configuration files like the `charmcraft.yaml` file mentioned above) that give Juju, Python, or Charmcraft various bits of information about your charm, and - executable files (like the `src/charm.py` file that we will see shortly) where you will use Ops-enriched Python to write all the logic of your charm. +## Create a charm project -## Set the basic information, requirements, and workload for your charm +In your virtual machine, go into your project directory and create the initial version of your charm: -Create a file called `charmcraft.yaml`. This is a file that describes metadata such as the charm name, purpose, environment constraints, workload containers, etc., in short, all the information that tells Juju what it can do with your charm. +```text +cd ~/fastapi-demo +charmcraft init --profile kubernetes +``` -In this file, do all of the following: +Charmcraft created several files, including: -First, add basic information about your charm: +- `charmcraft.yaml` - Metadata about your charm. Used by Juju and Charmcraft. +- `pyproject.toml` - Python project configuration. Lists the dependencies of your charm. +- `src/charm.py` - The Python file that will contain the main logic of your charm. -```text -name: demo-api-charm -title: | - demo-fastapi-k8s -description: | - This is a demo charm built on top of a small Python FastAPI server. -summary: | - FastAPI Demo charm for Kubernetes -``` +These files currently contain placeholder code and configuration. -Second, add a constraint assuming a Juju version with the required features and a Kubernetes-type cloud: +## Write your charm -```text -assumes: - - juju >= 3.1 - - k8s-api +### Edit the metadata + +Open `~/k8s-tutorial/charmcraft.yaml` in your usual text editor or IDE, then change the values of `title`, `summary`, and `description` to: + +```yaml +title: Web Server Demo +summary: A demo charm that operates a small Python FastAPI server. +description: | + This charm demonstrates how to write a Kubernetes charm with Ops. ``` -Third, describe the workload container, as below. Below, `demo-server` is the name of the container, and `demo-server-image` is the name of its OCI image. +Next, describe the workload container and its OCI image. -```text +In `charmcraft.yaml`, replace the `containers` and `resources` blocks with: + +```yaml containers: demo-server: resource: demo-server-image -``` - -Fourth, describe the workload container resources, as below. The name of the resource below, `demo-server-image`, is the one you defined above. - -```text resources: - # An OCI image resource for each container listed above. - # You may remove this if your charm will run without a workload sidecar container. + # An OCI image resource for the container listed above. demo-server-image: type: oci-image description: OCI image from GitHub Container Repository - # The upstream-source field is ignored by Juju. It is included here as a reference - # so the integration testing suite knows which image to deploy during testing. This field - # is also used by the 'canonical/charming-actions' GitHub action for automated releasing. + # The upstream-source field is ignored by Charmcraft and Juju, but it can be + # useful to developers in identifying the source of the OCI image. It is also + # used by the 'canonical/charming-actions' GitHub action for automated releases. + # The test_deploy function in tests/integration/test_charm.py reads upstream-source + # to determine which OCI image to use when running the charm's integration tests. upstream-source: ghcr.io/canonical/api_demo_server:1.0.1 ``` +### Define the charm class +We'll now write the charm code that handles events from Juju. Charmcraft created `src/charm.py` as the location for this logic. -## Define the charm initialisation and application services - - - -Create a file called `requirements.txt`. This is a file that describes all the required external Python dependencies that will be used by your charm. - - -In this file, declare the `ops` dependency, as below. At this point you're ready to start using constructs from the Ops library. - -``` -ops>2,<4 -``` - - -Create a file called `src/charm.py`. This is the file that you will use to write all the Python code that you want your charm to execute in response to events it receives from the Juju controller. - - -This file needs to be executable. One way you can do this is: - -```text -chmod a+x src/charm.py -``` - -In this file, do all of the following: - -First, add a shebang to ensure that the file is directly executable. Then, import the `ops` package to access the`CharmBase` class and the `main` function. Next, use `CharmBase` to create a charm class `FastAPIDemoCharm` and then invoke this class in the `main` function of Ops. As you can see, a charm is a pure Python class that inherits from the CharmBase class of Ops and which we pass to the `main` function defined in the `ops.main` module. +Replace the contents of `src/charm.py` with: ```python #!/usr/bin/env python3 @@ -141,12 +117,15 @@ class FastAPIDemoCharm(ops.CharmBase): super().__init__(framework) -if __name__ == '__main__': # pragma: nocover +if __name__ == "__main__": # pragma: nocover ops.main(FastAPIDemoCharm) ``` +As you can see, a charm is a pure Python class that inherits from the `CharmBase` class of Ops and which we pass to the `main` function defined in the `ops.main` module. We'll refer to `FastAPIDemoCharm` as the "charm class". -Now, in the `__init__` function of your charm class, use Ops constructs to add an observer for when the Juju controller informs the charm that the Pebble in its workload container is up and running, as below. As you can see, the observer is a function that takes as an argument an event and an event handler. The event name is created automatically by Ops for each container on the template `-pebble-ready`. The event handler is a method in your charm class that will be executed when the event is fired; in this case, you will use it to tell Pebble how to start your application. +### Handle the pebble-ready event + +In the `__init__` function of your charm class, use Ops constructs to add an observer for when the Juju controller informs the charm that the Pebble in its workload container is up and running, as below. As you can see, the observer is a function that takes as an argument an event and an event handler. The event name is created automatically by Ops for each container on the template `-pebble-ready`. The event handler is a method in your charm class that will be executed when the event is fired; in this case, you will use it to tell Pebble how to start your application. ```python framework.observe(self.on.demo_server_pebble_ready, self._on_demo_server_pebble_ready) @@ -195,20 +174,11 @@ In case it helps, the definition of a Pebble layer is very similar to the defini ```python def _on_demo_server_pebble_ready(self, event: ops.PebbleReadyEvent) -> None: - """Define and start a workload using the Pebble API. - - Change this example to suit your needs. You'll need to specify the right entrypoint and - environment configuration for your specific workload. - - Learn more about interacting with Pebble at - https://documentation.ubuntu.com/ops/latest/reference/pebble/ - Learn more about Pebble layers at - https://documentation.ubuntu.com/pebble/how-to/use-layers/ - """ + """Define and start a workload using the Pebble API.""" # Get a reference the container attribute on the PebbleReadyEvent container = event.workload # Add initial Pebble config layer using the Pebble API - container.add_layer('fastapi_demo', self._get_pebble_layer(), combine=True) + container.add_layer("fastapi_demo", self._get_pebble_layer(), combine=True) # Make Pebble reevaluate its plan, ensuring any services are started if enabled. container.replan() # Learn more about statuses at @@ -221,7 +191,7 @@ The custom Pebble layer that you just added is defined in the `self._get_pebble In the `__init__` method of your charm class, name your service to `fastapi-service` and add it as a class attribute : ```python -self.pebble_service_name = 'fastapi-service' +self.pebble_service_name = "fastapi-service" ``` Finally, define the `_get_pebble_layer` function as below. The `command` variable represents a command line that should be executed in order to start our application. @@ -229,34 +199,32 @@ Finally, define the `_get_pebble_layer` function as below. The `command` variab ```python def _get_pebble_layer(self) -> ops.pebble.Layer: """Pebble layer for the FastAPI demo services.""" - command = ' '.join( + command = " ".join( [ - 'uvicorn', - 'api_demo_server.app:app', - '--host=0.0.0.0', - '--port=8000', + "uvicorn", + "api_demo_server.app:app", + "--host=0.0.0.0", + "--port=8000", ] ) pebble_layer: ops.pebble.LayerDict = { - 'summary': 'FastAPI demo service', - 'description': 'pebble config layer for FastAPI demo server', - 'services': { + "summary": "FastAPI demo service", + "description": "pebble config layer for FastAPI demo server", + "services": { self.pebble_service_name: { - 'override': 'replace', - 'summary': 'fastapi demo', - 'command': command, - 'startup': 'enabled', + "override": "replace", + "summary": "fastapi demo", + "command": command, + "startup": "enabled", } }, } return ops.pebble.Layer(pebble_layer) ``` +### Add logger functionality - -## Add logger functionality - -In the `src/charm.py` file, in the imports section, import the Python `logging` module and define a logger object, as below. This will allow you to read log data in `juju`. +In the imports section of `src/charm.py`, import the Python `logging` module and define a logger object, as below. This will allow you to read log data in `juju`. ```python import logging @@ -265,40 +233,9 @@ import logging logger = logging.getLogger(__name__) ``` -## Tell Charmcraft how to build your charm - -In the same `charmcraft.yaml` file you created earlier, you need to describe all the information needed for Charmcraft to be able to pack your charm. In this file, do the following: - -First, add the block below. This will identify your charm as a charm (as opposed to something else you might know from using Juju, namely, a bundle). - -``` -type: charm -``` - - -Also add the block below. This declares that your charm will build and run charm on Ubuntu 22.04. - -``` -bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" -``` - - -And that's it! Time to validate your charm! - -```{tip} - -Once you've mastered the basics, you can speed things up by navigating to your empty charm project directory and running `charmcraft init --profile kubernetes`. This will create all the files above and more, along with helpful descriptor keys and code scaffolding. - -``` - +## Try your charm -## Validate your charm +### Pack your charm First, ensure that you are inside the Multipass Ubuntu VM, in the `~/fastapi-demo` folder: @@ -307,11 +244,11 @@ multipass shell charm-dev cd ~/fastapi-demo ``` -Now, pack your charm project directory into a `.charm` file, as below. This will produce a `.charm` file. In our case it was named `demo-api-charm_ubuntu-22.04-amd64.charm`; yours should be named similarly, though the name might vary slightly depending on your architecture. +Now, pack your charm project directory into a `.charm` file, as below. This will produce a `.charm` file. In our case it was named `fastapi-demo_ubuntu-22.04-amd64.charm`; yours should be named similarly, though the name might vary slightly depending on your architecture. ``` charmcraft pack -# Packed demo-api-charm_ubuntu-22.04-amd64.charm +# Packed fastapi-demo_ubuntu-22.04-amd64.charm ``` ```{important} @@ -338,10 +275,12 @@ This name might vary slightly, depending on your architecture. E.g., for an `arm ``` --> +### Deploy your charm + Deploy the `.charm` file, as below. Juju will create a Kubernetes `StatefulSet` named after your application with one replica. ```text -juju deploy ./demo-api-charm_ubuntu-22.04-amd64.charm --resource \ +juju deploy ./fastapi-demo_ubuntu-22.04-amd64.charm --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` @@ -365,14 +304,16 @@ When all units are settled down, you should see the output below, where `10.152. Model Controller Cloud/Region Version SLA Timestamp welcome-k8s microk8s microk8s/localhost 3.6.8 unsupported 13:38:19+01:00 -App Version Status Scale Charm Channel Rev Address Exposed Message -demo-api-charm active 1 demo-api-charm 0 10.152.183.215 no +App Version Status Scale Charm Channel Rev Address Exposed Message +fastapi-demo active 1 fastapi-demo 0 10.152.183.215 no -Unit Workload Agent Address Ports Message -demo-api-charm/0* active idle 10.1.157.73 +Unit Workload Agent Address Ports Message +fastapi-demo/0* active idle 10.1.157.73 ``` -Now, validate that the app is running and reachable by sending an HTTP request as below, where `10.1.157.73` is the IP of our pod and `8000` is the default application port. +### Try the web server + +Validate that the app is running and reachable by sending an HTTP request as below, where `10.1.157.73` is the IP of our pod and `8000` is the default application port. ``` curl 10.1.157.73:8000/version @@ -381,7 +322,7 @@ curl 10.1.157.73:8000/version You should see a JSON string with the version of the application: ``` -{"version":"1.0.0"} +{"version":"1.0.1"} ``` Congratulations, you've successfully created a minimal Kubernetes charm! @@ -407,13 +348,13 @@ You should see that your application has been deployed in a pod that has 2 conta ```text NAME READY STATUS RESTARTS AGE modeloperator-5df6588d89-ghxtz 1/1 Running 0 10m -demo-api-charm-0 2/2 Running 0 10m +fastapi-demo-0 2/2 Running 0 10m ``` 3. Check also: ```text -kubectl -n welcome-k8s describe pod demo-api-charm-0 +kubectl -n welcome-k8s describe pod fastapi-demo-0 ``` In the output you should see the definition for both containers. You'll be able to verify that the default command and arguments for our application container (`demo-server`) have been displaced by the Pebble service. You should be able to verify the same for the charm container (`charm`). @@ -685,14 +626,14 @@ tests/integration/test_charm.py::test_deploy -------------------------------- live log setup -------------------------------- INFO jubilant:_juju.py:227 cli: juju add-model --no-switch jubilant-823cf1fd -------------------------------- live log call --------------------------------- -INFO jubilant:_juju.py:227 cli: juju deploy --model jubilant-823cf1fd ./demo-api-charm_ubuntu-22.04-amd64.charm demo-api-charm --resource demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 +INFO jubilant:_juju.py:227 cli: juju deploy --model jubilant-823cf1fd ./fastapi-demo_ubuntu-22.04-amd64.charm fastapi-demo --resource demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 INFO jubilant.wait:_juju.py:1164 wait: status changed: + .model.name = 'jubilant-823cf1fd' ... INFO jubilant.wait:_juju.py:1164 wait: status changed: -- .apps['demo-api-charm'].app_status.current = 'waiting' -- .apps['demo-api-charm'].app_status.message = 'installing agent' -+ .apps['demo-api-charm'].app_status.current = 'active' +- .apps['fastapi-demo'].app_status.current = 'waiting' +- .apps['fastapi-demo'].app_status.message = 'installing agent' ++ .apps['fastapi-demo'].app_status.current = 'active' PASSED ------------------------------ live log teardown ------------------------------- INFO jubilant:_juju.py:227 cli: juju destroy-model jubilant-823cf1fd --no-prompt --destroy-storage --force diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md index 710829372..068fa9603 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md @@ -109,22 +109,22 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./demo-api-charm_ubuntu-22.04-amd64.charm" \ - demo-api-charm --force-units --resource \ + --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` Next, test that the basic action invocation works: ```text -juju run demo-api-charm/0 get-db-info +juju run fastapi-demo/0 get-db-info ``` It might take a few seconds, but soon you should see an output similar to the one below, showing the database host and port: ```text Running operation 1 with 1 task - - task 2 on unit-demo-api-charm-0 + - task 2 on unit-fastapi-demo-0 Waiting for task 2... db-host: postgresql-k8s-primary.welcome-k8s.svc.cluster.local @@ -134,14 +134,14 @@ db-port: "5432" Now, test that the action parameter (`show-password`) works as well by setting it to `True`: ```text -juju run demo-api-charm/0 get-db-info show-password=True +juju run fastapi-demo/0 get-db-info show-password=True ``` The output should now include the username and the password: ```text Running operation 3 with 1 task - - task 4 on unit-demo-api-charm-0 + - task 4 on unit-fastapi-demo-0 Waiting for task 4... db-host: postgresql-k8s-primary.welcome-k8s.svc.cluster.local diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 9dbb1d1dc..6bfde9fc7 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -351,8 +351,8 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./demo-api-charm_ubuntu-22.04-amd64.charm" \ - demo-api-charm --force-units --resource \ + --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` @@ -365,7 +365,7 @@ juju deploy postgresql-k8s --channel=14/stable --trust Now, integrate our charm with the newly deployed `postgresql-k8s` charm: ```text -juju integrate postgresql-k8s demo-api-charm +juju integrate postgresql-k8s fastapi-demo ``` > Read more: {external+juju:ref}`Juju | Relation (integration) `, [`juju integrate`](inv:juju:std:label#command-juju-integrate) @@ -376,22 +376,22 @@ Finally, run: juju status --relations --watch 1s ``` -You should see both applications get to the `active` status, and also that the `postgresql-k8s` charm has a relation to the `demo-api-charm` over the `postgresql_client` interface, as below: +You should see both applications get to the `active` status, and also that the `postgresql-k8s` charm has a relation to the `fastapi-demo` over the `postgresql_client` interface, as below: ```text Model Controller Cloud/Region Version SLA Timestamp welcome-k8s microk8s microk8s/localhost 3.6.8 unsupported 13:50:39+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message -demo-api-charm active 1 demo-api-charm 2 10.152.183.233 no +fastapi-demo active 1 fastapi-demo 2 10.152.183.233 no postgresql-k8s 14.15 active 1 postgresql-k8s 14/stable 495 10.152.183.195 no Unit Workload Agent Address Ports Message -demo-api-charm/0* active idle 10.1.157.90 +fastapi-demo/0* active idle 10.1.157.90 postgresql-k8s/0* active idle 10.1.157.92 Primary Integration provider Requirer Interface Type Message -postgresql-k8s:database demo-api-charm:database postgresql_client regular +postgresql-k8s:database fastapi-demo:database postgresql_client regular postgresql-k8s:database-peers postgresql-k8s:database-peers postgresql_peers peer postgresql-k8s:restart postgresql-k8s:restart rolling_op peer postgresql-k8s:upgrade postgresql-k8s:upgrade upgrade peer @@ -547,9 +547,9 @@ When it's done, the output should show two passing tests: tests/integration/test_charm.py::test_deploy ... INFO jubilant.wait:_juju.py:1164 wait: status changed: -- .apps['demo-api-charm'].units['demo-api-charm/0'].juju_status.current = 'executing' -- .apps['demo-api-charm'].units['demo-api-charm/0'].juju_status.message = 'running start hook' -+ .apps['demo-api-charm'].units['demo-api-charm/0'].juju_status.current = 'idle' +- .apps['fastapi-demo'].units['fastapi-demo/0'].juju_status.current = 'executing' +- .apps['fastapi-demo'].units['fastapi-demo/0'].juju_status.message = 'running start hook' ++ .apps['fastapi-demo'].units['fastapi-demo/0'].juju_status.current = 'idle' PASSED ``` diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md index 6afa1e720..d7cdb0b93 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md @@ -174,21 +174,21 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./demo-api-charm_ubuntu-22.04-amd64.charm" \ - demo-api-charm --force-units --resource \ + --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` Now, check the available configuration options: ```text -juju config demo-api-charm +juju config fastapi-demo ``` Our newly defined `server-port` option is there. Let's try to configure it to something else, e.g., `5000`: ```text -juju config demo-api-charm server-port=5000 +juju config fastapi-demo server-port=5000 ``` Now, let's validate that the app is actually running and reachable on the new port by sending the HTTP request below, where `10.1.157.74` is the IP of our pod and `5000` is the new application port: @@ -202,7 +202,7 @@ You should see JSON string with the version of the application: `{"version":"1.0 Let's also verify that our invalid port number check works by setting the port to `22` and then running `juju status`: ```text -juju config demo-api-charm server-port=22 +juju config fastapi-demo server-port=22 juju status ``` @@ -213,10 +213,10 @@ Model Controller Cloud/Region Version SLA Timestamp welcome-k8s microk8s microk8s/localhost 3.6.8 unsupported 18:19:24+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message -demo-api-charm blocked 1 demo-api-charm 1 10.152.183.215 no Invalid port number, 22 is reserved for SSH +fastapi-demo blocked 1 fastapi-demo 1 10.152.183.215 no Invalid port number, 22 is reserved for SSH Unit Workload Agent Address Ports Message -demo-api-charm/0* blocked idle 10.1.157.74 Invalid port number, 22 is reserved for SSH +fastapi-demo/0* blocked idle 10.1.157.74 Invalid port number, 22 is reserved for SSH ``` Congratulations, you now know how to make your charm configurable! @@ -224,7 +224,7 @@ Congratulations, you now know how to make your charm configurable! Before continuing, reset the port to `8000` and check that the application is in `active` status: ```text -juju config demo-api-charm server-port=8000 +juju config fastapi-demo server-port=8000 juju status ``` diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md index 38bfb91fe..fcfb4e72a 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md @@ -236,8 +236,8 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./demo-api-charm_ubuntu-22.04-amd64.charm" \ - demo-api-charm --force-units --resource \ + --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` @@ -290,9 +290,9 @@ Now switch back to the charm model and integrate your charm with the exposed end ```text juju switch welcome-k8s -juju integrate demo-api-charm admin/cos-lite.grafana -juju integrate demo-api-charm admin/cos-lite.loki -juju integrate demo-api-charm admin/cos-lite.prometheus +juju integrate fastapi-demo admin/cos-lite.grafana +juju integrate fastapi-demo admin/cos-lite.loki +juju integrate fastapi-demo admin/cos-lite.prometheus ```

Access your applications from the host machine

From be333b18f085b13cb8df60d0a2666cad196ca1d7 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 15:15:28 +0800 Subject: [PATCH 05/14] change Juju model name --- .../create-a-minimal-kubernetes-charm.md | 10 +++++----- .../expose-operational-tasks-via-actions.md | 4 ++-- .../integrate-your-charm-with-postgresql.md | 4 ++-- .../make-your-charm-configurable.md | 4 ++-- .../observe-your-charm-with-cos-lite.md | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 18ab35b64..20d218e05 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -362,8 +362,8 @@ juju status --watch 1s When all units are settled down, you should see the output below, where `10.152.183.215` is the IP of the K8s Service and `10.1.157.73` is the IP of the pod. ```text -Model Controller Cloud/Region Version SLA Timestamp -welcome-k8s microk8s microk8s/localhost 3.6.8 unsupported 13:38:19+01:00 +Model Controller Cloud/Region Version SLA Timestamp +testing microk8s microk8s/localhost 3.6.8 unsupported 13:38:19+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm active 1 demo-api-charm 0 10.152.183.215 no @@ -394,12 +394,12 @@ Congratulations, you've successfully created a minimal Kubernetes charm! kubectl get namespaces ``` -You should see that Juju has created a namespace called `welcome-k8s`. +You should see that Juju has created a namespace called `testing`. 2. Try: ```text -kubectl -n welcome-k8s get pods +kubectl -n testing get pods ``` You should see that your application has been deployed in a pod that has 2 containers running in it, one for the charm and one for the application. The containers talk to each other via the Pebble API using the UNIX socket. @@ -413,7 +413,7 @@ demo-api-charm-0 2/2 Running 0 10m 3. Check also: ```text -kubectl -n welcome-k8s describe pod demo-api-charm-0 +kubectl -n testing describe pod demo-api-charm-0 ``` In the output you should see the definition for both containers. You'll be able to verify that the default command and arguments for our application container (`demo-server`) have been displaced by the Pebble service. You should be able to verify the same for the charm container (`charm`). diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md index 710829372..88004a271 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md @@ -127,7 +127,7 @@ Running operation 1 with 1 task - task 2 on unit-demo-api-charm-0 Waiting for task 2... -db-host: postgresql-k8s-primary.welcome-k8s.svc.cluster.local +db-host: postgresql-k8s-primary.testing.svc.cluster.local db-port: "5432" ``` @@ -144,7 +144,7 @@ Running operation 3 with 1 task - task 4 on unit-demo-api-charm-0 Waiting for task 4... -db-host: postgresql-k8s-primary.welcome-k8s.svc.cluster.local +db-host: postgresql-k8s-primary.testing.svc.cluster.local db-password: RGv80aF9WAJJtExn db-port: "5432" db-username: relation_id_4 diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 04cca1af1..24affa0b2 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -379,8 +379,8 @@ juju status --relations --watch 1s You should see both applications get to the `active` status, and also that the `postgresql-k8s` charm has a relation to the `demo-api-charm` over the `postgresql_client` interface, as below: ```text -Model Controller Cloud/Region Version SLA Timestamp -welcome-k8s microk8s microk8s/localhost 3.6.8 unsupported 13:50:39+01:00 +Model Controller Cloud/Region Version SLA Timestamp +testing microk8s microk8s/localhost 3.6.8 unsupported 13:50:39+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm active 1 demo-api-charm 2 10.152.183.233 no diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md index 6afa1e720..e66f6fadc 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md @@ -209,8 +209,8 @@ juju status As expected, the application is indeed in the `blocked` state: ```text -Model Controller Cloud/Region Version SLA Timestamp -welcome-k8s microk8s microk8s/localhost 3.6.8 unsupported 18:19:24+01:00 +Model Controller Cloud/Region Version SLA Timestamp +testing microk8s microk8s/localhost 3.6.8 unsupported 18:19:24+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm blocked 1 demo-api-charm 1 10.152.183.215 no Invalid port number, 22 is reserved for SSH diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md index 6f2deffb6..5c88360fa 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md @@ -282,14 +282,14 @@ microk8s admin/cos-lite.prometheus admin prometheus_scrape:metrics-endpoint microk8s admin/cos-lite.grafana admin grafana_dashboard:grafana-dashboard ``` -As you might notice from your knowledge of Juju, this is essentially preparing these endpoints, which exist in the `cos-lite` model, for a cross-model relation with your charm, which you've deployed to the `welcome-k8s` model. +As you might notice from your knowledge of Juju, this is essentially preparing these endpoints, which exist in the `cos-lite` model, for a cross-model relation with your charm, which you've deployed to the `testing` model. ## Integrate your charm with COS Lite Now switch back to the charm model and integrate your charm with the exposed endpoints, as below. This effectively integrates your application with Prometheus, Loki, and Grafana. ```text -juju switch welcome-k8s +juju switch testing juju integrate demo-api-charm admin/cos-lite.grafana juju integrate demo-api-charm admin/cos-lite.loki juju integrate demo-api-charm admin/cos-lite.prometheus @@ -390,7 +390,7 @@ http://10.152.183.132:3000/?orgId=1&search=open Click on `FastAPI Monitoring` --> -Next, in the `Juju model` drop down field, select `welcome-k8s`. +Next, in the `Juju model` drop down field, select `testing`. Now, call a couple of API points on the application, as below. To produce some successful requests and some requests with code 500 (internal server error), call several times, in any order. From db4c0dc658f15b99d76b9f675a95f985d7d22731 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 15:44:28 +0800 Subject: [PATCH 06/14] update packed charm name --- .../create-a-minimal-kubernetes-charm.md | 8 ++++---- .../expose-operational-tasks-via-actions.md | 2 +- .../integrate-your-charm-with-postgresql.md | 2 +- .../make-your-charm-configurable.md | 2 +- .../observe-your-charm-with-cos-lite.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index dc1f945d6..773915ab0 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -244,11 +244,11 @@ multipass shell juju-sandbox-k8s cd ~/fastapi-demo ``` -Now, pack your charm project directory into a `.charm` file, as below. This will produce a `.charm` file. In our case it was named `fastapi-demo_ubuntu-22.04-amd64.charm`; yours should be named similarly, though the name might vary slightly depending on your architecture. +Now, pack your charm project directory into a `.charm` file, as below. This will produce a `.charm` file. In our case it was named `fastapi-demo_amd64.charm`; yours should be named similarly, though the name might vary slightly depending on your architecture. ``` charmcraft pack -# Packed fastapi-demo_ubuntu-22.04-amd64.charm +# Packed fastapi-demo_amd64.charm ``` ```{important} @@ -280,7 +280,7 @@ This name might vary slightly, depending on your architecture. E.g., for an `arm Deploy the `.charm` file, as below. Juju will create a Kubernetes `StatefulSet` named after your application with one replica. ```text -juju deploy ./fastapi-demo_ubuntu-22.04-amd64.charm --resource \ +juju deploy ./fastapi-demo_amd64.charm --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` @@ -626,7 +626,7 @@ tests/integration/test_charm.py::test_deploy -------------------------------- live log setup -------------------------------- INFO jubilant:_juju.py:227 cli: juju add-model --no-switch jubilant-823cf1fd -------------------------------- live log call --------------------------------- -INFO jubilant:_juju.py:227 cli: juju deploy --model jubilant-823cf1fd ./fastapi-demo_ubuntu-22.04-amd64.charm fastapi-demo --resource demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 +INFO jubilant:_juju.py:227 cli: juju deploy --model jubilant-823cf1fd ./fastapi-demo_amd64.charm fastapi-demo --resource demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 INFO jubilant.wait:_juju.py:1164 wait: status changed: + .model.name = 'jubilant-823cf1fd' ... diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md index 1238adc74..36bd57d80 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/expose-operational-tasks-via-actions.md @@ -109,7 +109,7 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + --path="./fastapi-demo_amd64.charm" \ fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 7cdfc864e..85e28ab83 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -351,7 +351,7 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + --path="./fastapi-demo_amd64.charm" \ fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md index d5de7f69d..5dc085b56 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md @@ -174,7 +174,7 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + --path="./fastapi-demo_amd64.charm" \ fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md index 98d6fc3ba..99308f094 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md @@ -236,7 +236,7 @@ First, repack and refresh your charm: ```text charmcraft pack juju refresh \ - --path="./fastapi-demo_ubuntu-22.04-amd64.charm" \ + --path="./fastapi-demo_amd64.charm" \ fastapi-demo --force-units --resource \ demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 ``` From 199df1ff94e9705f02c09e2fbb81ba607e16851e Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 15:51:08 +0800 Subject: [PATCH 07/14] update Juju version --- .../create-a-minimal-kubernetes-charm.md | 2 +- .../integrate-your-charm-with-postgresql.md | 2 +- .../make-your-charm-configurable.md | 2 +- .../observe-your-charm-with-cos-lite.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 20d218e05..3326055ce 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -363,7 +363,7 @@ When all units are settled down, you should see the output below, where `10.152. ```text Model Controller Cloud/Region Version SLA Timestamp -testing microk8s microk8s/localhost 3.6.8 unsupported 13:38:19+01:00 +testing microk8s microk8s/localhost 3.6.12 unsupported 13:38:19+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm active 1 demo-api-charm 0 10.152.183.215 no diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 24affa0b2..1acc5e6da 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -380,7 +380,7 @@ You should see both applications get to the `active` status, and also that the ` ```text Model Controller Cloud/Region Version SLA Timestamp -testing microk8s microk8s/localhost 3.6.8 unsupported 13:50:39+01:00 +testing microk8s microk8s/localhost 3.6.12 unsupported 13:50:39+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm active 1 demo-api-charm 2 10.152.183.233 no diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md index e66f6fadc..182727377 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md @@ -210,7 +210,7 @@ As expected, the application is indeed in the `blocked` state: ```text Model Controller Cloud/Region Version SLA Timestamp -testing microk8s microk8s/localhost 3.6.8 unsupported 18:19:24+01:00 +testing microk8s microk8s/localhost 3.6.12 unsupported 18:19:24+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm blocked 1 demo-api-charm 1 10.152.183.215 no Invalid port number, 22 is reserved for SSH diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md index 5c88360fa..4490b77f2 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md @@ -312,7 +312,7 @@ This should result in an output similar to the one below: ```text Model Controller Cloud/Region Version SLA Timestamp -cos-lite microk8s microk8s/localhost 3.6.8 unsupported 18:05:07+01:00 +cos-lite microk8s microk8s/localhost 3.6.12 unsupported 18:05:07+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message alertmanager 0.27.0 active 1 alertmanager-k8s 1/stable 160 10.152.183.70 no From 72bea0fb244dc61d26405246b3ad3a1864ec5594 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 15:56:58 +0800 Subject: [PATCH 08/14] change Juju controller name --- .../create-a-minimal-kubernetes-charm.md | 4 ++-- .../integrate-your-charm-with-postgresql.md | 4 ++-- .../make-your-charm-configurable.md | 4 ++-- .../observe-your-charm-with-cos-lite.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 3326055ce..800f2d695 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -362,8 +362,8 @@ juju status --watch 1s When all units are settled down, you should see the output below, where `10.152.183.215` is the IP of the K8s Service and `10.1.157.73` is the IP of the pod. ```text -Model Controller Cloud/Region Version SLA Timestamp -testing microk8s microk8s/localhost 3.6.12 unsupported 13:38:19+01:00 +Model Controller Cloud/Region Version SLA Timestamp +testing concierge-microk8s microk8s/localhost 3.6.12 unsupported 13:38:19+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm active 1 demo-api-charm 0 10.152.183.215 no diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 1acc5e6da..31230d8f2 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -379,8 +379,8 @@ juju status --relations --watch 1s You should see both applications get to the `active` status, and also that the `postgresql-k8s` charm has a relation to the `demo-api-charm` over the `postgresql_client` interface, as below: ```text -Model Controller Cloud/Region Version SLA Timestamp -testing microk8s microk8s/localhost 3.6.12 unsupported 13:50:39+01:00 +Model Controller Cloud/Region Version SLA Timestamp +testing concierge-microk8s microk8s/localhost 3.6.12 unsupported 13:50:39+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm active 1 demo-api-charm 2 10.152.183.233 no diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md index 182727377..587aad60c 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md @@ -209,8 +209,8 @@ juju status As expected, the application is indeed in the `blocked` state: ```text -Model Controller Cloud/Region Version SLA Timestamp -testing microk8s microk8s/localhost 3.6.12 unsupported 18:19:24+01:00 +Model Controller Cloud/Region Version SLA Timestamp +testing concierge-microk8s microk8s/localhost 3.6.12 unsupported 18:19:24+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message demo-api-charm blocked 1 demo-api-charm 1 10.152.183.215 no Invalid port number, 22 is reserved for SSH diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md index 4490b77f2..b5e85b299 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/observe-your-charm-with-cos-lite.md @@ -311,8 +311,8 @@ juju status -m cos-lite This should result in an output similar to the one below: ```text -Model Controller Cloud/Region Version SLA Timestamp -cos-lite microk8s microk8s/localhost 3.6.12 unsupported 18:05:07+01:00 +Model Controller Cloud/Region Version SLA Timestamp +cos-lite concierge-microk8s microk8s/localhost 3.6.12 unsupported 18:05:07+01:00 App Version Status Scale Charm Channel Rev Address Exposed Message alertmanager 0.27.0 active 1 alertmanager-k8s 1/stable 160 10.152.183.70 no From 43c40cdd43ff25baf46cd6f523adcbaf1150fbaf Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 16:00:12 +0800 Subject: [PATCH 09/14] fix version returned from server --- .../create-a-minimal-kubernetes-charm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 72b8e8466..bda15bff6 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -322,7 +322,7 @@ curl 10.1.157.73:8000/version You should see a JSON string with the version of the application: ``` -{"version":"1.0.1"} +{"version":"1.0.0"} ``` Congratulations, you've successfully created a minimal Kubernetes charm! From 33df20933cdd103e8c286950ad0af4239f517690 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 16:59:28 +0800 Subject: [PATCH 10/14] fix table formatting --- .../create-a-minimal-kubernetes-charm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index bda15bff6..b54d2530f 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -348,7 +348,7 @@ You should see that your application has been deployed in a pod that has 2 conta ```text NAME READY STATUS RESTARTS AGE modeloperator-5df6588d89-ghxtz 1/1 Running 0 10m -fastapi-demo-0 2/2 Running 0 10m +fastapi-demo-0 2/2 Running 0 10m ``` 3. Check also: From 471e7d361eea1d853a9055644f91d76dfb090520 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 17:20:10 +0800 Subject: [PATCH 11/14] add a note about the workload module --- .../create-a-minimal-kubernetes-charm.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index b54d2530f..2f799754a 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -57,10 +57,12 @@ Charmcraft created several files, including: - `charmcraft.yaml` - Metadata about your charm. Used by Juju and Charmcraft. - `pyproject.toml` - Python project configuration. Lists the dependencies of your charm. -- `src/charm.py` - The Python file that will contain the main logic of your charm. +- `src/charm.py` - The Python file that will contain the logic of your charm. These files currently contain placeholder code and configuration. +Charmcraft also created a module called `src/fastapi_demo.py`. We won't need this module. In general, it's a good place to put functions that interact with the running workload. + ## Write your charm ### Edit the metadata From 031dd13cb812c8f78a1f4199b83be4a1ad0286b6 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 17:47:09 +0800 Subject: [PATCH 12/14] update unit tests --- .../create-a-minimal-kubernetes-charm.md | 100 ++++-------------- 1 file changed, 23 insertions(+), 77 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 2f799754a..524f36198 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -374,64 +374,9 @@ We'll also use the Python testing tool [`tox`](https://tox.wiki/en/4.14.2/index. In this section we'll write a test to check that Pebble is configured as expected. -### Prepare your test environment - -Create a file called `tox.ini` in your project root directory and add the following configuration: - -``` -[tox] -no_package = True -skip_missing_interpreters = True -min_version = 4.0.0 -env_list = unit - -[vars] -src_path = {tox_root}/src -tests_path = {tox_root}/tests -all_path = {[vars]src_path} {[vars]tests_path} - -[testenv] -set_env = - PYTHONPATH = {tox_root}/lib:{[vars]src_path} - PYTHONBREAKPOINT=pdb.set_trace - PY_COLORS=1 -pass_env = - PYTHONPATH - CHARM_BUILD_DIR - MODEL_SETTINGS - -[testenv:unit] -description = Run unit tests -deps = - pytest - cosl - coverage[toml] - ops[testing] - -r {tox_root}/requirements.txt -commands = - coverage run --source={[vars]src_path} -m pytest \ - -v \ - -s \ - --tb native \ - {[vars]tests_path}/unit \ - {posargs} - coverage report -``` -> Read more: [`tox.ini`](https://tox.wiki/en/latest/config.html#tox-ini) - -If you used `charmcraft init --profile kubernetes` at the beginning of your project, you will already have the `tox.ini` file. - -### Prepare your test directory - -In your project root directory, create directory for the unit test: - -```text -mkdir -p tests/unit -``` - ### Write a test -In your `tests/unit` directory, create a new file called `test_charm.py` and add the test below. This test will check the behaviour of the `_on_demo_server_pebble_ready` function that you set up earlier. The test will first set up a context, then define the input state, run the action, and check whether the results match the expected values. +Replace the contents of `tests/unit/test_charm.py` with: ```python import ops @@ -442,7 +387,7 @@ from charm import FastAPIDemoCharm def test_pebble_layer(): ctx = testing.Context(FastAPIDemoCharm) - container = testing.Container(name='demo-server', can_connect=True) + container = testing.Container(name="demo-server", can_connect=True) state_in = testing.State( containers={container}, leader=True, @@ -450,12 +395,12 @@ def test_pebble_layer(): state_out = ctx.run(ctx.on.pebble_ready(container), state_in) # Expected plan after Pebble ready with default config expected_plan = { - 'services': { - 'fastapi-service': { - 'override': 'replace', - 'summary': 'fastapi demo', - 'command': 'uvicorn api_demo_server.app:app --host=0.0.0.0 --port=8000', - 'startup': 'enabled', + "services": { + "fastapi-service": { + "override": "replace", + "summary": "fastapi demo", + "command": "uvicorn api_demo_server.app:app --host=0.0.0.0 --port=8000", + "startup": "enabled", # Since the environment is empty, Layer.to_dict() will not # include it. } @@ -468,12 +413,13 @@ def test_pebble_layer(): assert state_out.unit_status == testing.ActiveStatus() # Check the service was started: assert ( - state_out.get_container(container.name).service_statuses['fastapi-service'] + state_out.get_container(container.name).service_statuses["fastapi-service"] == ops.pebble.ServiceStatus.ACTIVE ) - ``` +This test checks the behaviour of the `_on_demo_server_pebble_ready` function that you set up earlier. The test simulates your charm receiving the pebble-ready event, then checks that the unit and workload container have the correct state. + ### Run the test In your Multipass Ubuntu VM shell, run your test: @@ -485,25 +431,25 @@ ubuntu@juju-sandbox-k8s:~/fastapi-demo$ tox -e unit The result should be similar to the following output: ```text -unit: install_deps> python -I -m pip install cosl 'coverage[toml]' 'ops[testing]' pytest -r /home/ubuntu/fastapi-demo/requirements.txt -unit: commands[0]> coverage run --source=/home/ubuntu/fastapi-demo/src -m pytest -v -s --tb native /home/ubuntu/fastapi-demo/tests/unit -==================================================================================== test session starts ===================================================================================== -platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /home/ubuntu/fastapi-demo/.tox/unit/bin/python +... +============================================ test session starts ============================================= +platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /home/ubuntu/fastapi-demo/.tox/unit/bin/python3 cachedir: .tox/unit/.pytest_cache rootdir: /home/ubuntu/fastapi-demo +configfile: pyproject.toml collected 1 item tests/unit/test_charm.py::test_pebble_layer PASSED -===================================================================================== 1 passed in 0.19s ====================================================================================== +============================================= 1 passed in 1.21s ============================================== unit: commands[1]> coverage report -Name Stmts Miss Cover ----------------------------------- -src/charm.py 17 0 100% ----------------------------------- -TOTAL 17 0 100% - unit: OK (12.33=setup[11.76]+cmd[0.50,0.07] seconds) - congratulations :) (12.42 seconds) +Name Stmts Miss Branch BrPart Cover Missing +---------------------------------------------------------- +src/charm.py 18 0 0 0 100% +---------------------------------------------------------- +TOTAL 18 0 0 0 100% + unit: OK (4.26=setup[0.23]+cmd[3.33,0.70] seconds) + congratulations :) (4.30 seconds) ``` Congratulations, you have written your first unit test! From ed35b3a47b81568e772e898a9df91fc145f0f4e4 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 18:07:21 +0800 Subject: [PATCH 13/14] update integration tests --- .../create-a-minimal-kubernetes-charm.md | 129 ++++-------------- 1 file changed, 30 insertions(+), 99 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 524f36198..eff806242 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -422,10 +422,10 @@ This test checks the behaviour of the `_on_demo_server_pebble_ready` function th ### Run the test -In your Multipass Ubuntu VM shell, run your test: +Run the following command from anywhere in the `~/fastapi-demo` directory: ```text -ubuntu@juju-sandbox-k8s:~/fastapi-demo$ tox -e unit +tox -e unit ``` The result should be similar to the following output: @@ -466,68 +466,11 @@ You can ensure this by writing integration tests for your charm. In the charming In this section we'll write a small integration test to check that the charm packs and deploys correctly. -### Prepare your test environment - -In your `tox.ini` file, add the following new environment: - -``` -[testenv:integration] -description = Run integration tests -deps = - pytest - jubilant - -r {tox_root}/requirements.txt -commands = - pytest \ - -v \ - -s \ - --tb native \ - --log-cli-level=INFO \ - {[vars]tests_path}/integration \ - {posargs} -``` - -If you used `charmcraft init --profile kubernetes` at the beginning of your project, the `testenv:integration` section is already in the `tox.ini` file. - -### Prepare your test directory - -In your project root directory, create a directory for the integration test: - -```text -mkdir -p tests/integration -``` - -### Write and run a pack-and-deploy integration test - -Let's begin with the simplest possible integration test, a [smoke test](https://en.wikipedia.org/wiki/Smoke_testing_(software)). This test will build and deploy the charm, then verify that the installation event is handled without errors. - -In your `tests/integration` directory, create a file called `conftest.py` and add the following fixtures: - -```python -import pathlib -import subprocess - -import jubilant -import pytest - - -@pytest.fixture(scope='module') -def juju(request: pytest.FixtureRequest): - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - log = juju.debug_log(limit=1000) - print(log, end='') - +### Write a test -@pytest.fixture(scope='session') -def charm(): - subprocess.check_call(['charmcraft', 'pack']) # noqa - return next(pathlib.Path('.').glob('*.charm')) -``` +Let's write the simplest possible integration test, a [smoke test](https://en.wikipedia.org/wiki/Smoke_testing_(software)). This test will deploy the charm, then verify that the installation event is handled without errors. -In the same directory, create a file called `test_charm.py` and add the following test: +Replace the contents of `tests/integration/test_charm.py` with: ```python import logging @@ -538,58 +481,46 @@ import yaml logger = logging.getLogger(__name__) -METADATA = yaml.safe_load(pathlib.Path('./charmcraft.yaml').read_text()) -APP_NAME = METADATA['name'] +METADATA = yaml.safe_load(pathlib.Path("charmcraft.yaml").read_text()) +APP_NAME = METADATA["name"] def test_deploy(charm: pathlib.Path, juju: jubilant.Juju): - """Build the charm-under-test and deploy it together with related charms. - - Assert on the unit status before any relations/configurations take place. - """ + """Deploy the charm under test.""" resources = { - 'demo-server-image': METADATA['resources']['demo-server-image']['upstream-source'] + "demo-server-image": METADATA["resources"]["demo-server-image"]["upstream-source"] } - - # Deploy the charm and wait for active/idle status - juju.deploy(f'./{charm}', app=APP_NAME, resources=resources) + juju.deploy(charm.resolve(), app=APP_NAME, resources=resources) juju.wait(jubilant.all_active) ``` +This test depends on two fixtures, which are defined in `tests/integration/conftest.py`: + +- `charm` - The `.charm` file to deploy. +- `juju` - A Jubilant object for interacting with a temporary Juju model. + +### Run the test + +Run the following command from anywhere in the `~/fastapi-demo` directory: + +```text +tox -e integration +``` + The test takes some time to run as Jubilant adds a new model to an existing cluster (whose presence it assumes). If successful, it'll verify that your charm can pack and deploy as expected. The result should be similar to the following output: ```text -integration: commands[0]> pytest -v -s --tb native --log-cli-level=INFO /home/ubuntu/fastapi-demo/tests/integration -============================= test session starts ============================== -platform linux -- Python 3.10.18, pytest-8.4.1, pluggy-1.6.0 -- /home/ubuntu/fastapi-demo/.tox/integration/bin/python3 -cachedir: .tox/integration/.pytest_cache -rootdir: /home/ubuntu/fastapi-demo -configfile: pyproject.toml -collected 1 item +... -tests/integration/test_charm.py::test_deploy +============================= 1 passed in 55.43s ============================= + integration: OK (57.79=setup[0.23]+cmd[57.57] seconds) + congratulations :) (57.84 seconds) +``` --------------------------------- live log setup -------------------------------- -INFO jubilant:_juju.py:227 cli: juju add-model --no-switch jubilant-823cf1fd --------------------------------- live log call --------------------------------- -INFO jubilant:_juju.py:227 cli: juju deploy --model jubilant-823cf1fd ./fastapi-demo_amd64.charm fastapi-demo --resource demo-server-image=ghcr.io/canonical/api_demo_server:1.0.1 -INFO jubilant.wait:_juju.py:1164 wait: status changed: -+ .model.name = 'jubilant-823cf1fd' -... -INFO jubilant.wait:_juju.py:1164 wait: status changed: -- .apps['fastapi-demo'].app_status.current = 'waiting' -- .apps['fastapi-demo'].app_status.message = 'installing agent' -+ .apps['fastapi-demo'].app_status.current = 'active' -PASSED ------------------------------- live log teardown ------------------------------- -INFO jubilant:_juju.py:227 cli: juju destroy-model jubilant-823cf1fd --no-prompt --destroy-storage --force - - -========================= 1 passed in 63.92s (0:01:03) ========================= - integration: OK (64.10=setup[0.01]+cmd[64.10] seconds) - congratulations :) (64.15 seconds) +```{tip} +`tox -e integration` doesn't pack your charm. If you modify the charm code and want to run the integration tests again, run `charmcraft pack` before `tox -e integration`. ``` ## Review the final code From c65ddb31e961a7525fad8ae05cf05ce7fac35df8 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 31 Dec 2025 18:09:59 +0800 Subject: [PATCH 14/14] update code for charm 1 --- .../example-charm-integration-tests.yaml | 2 +- examples/k8s-1-minimal/charmcraft.yaml | 65 ++- examples/k8s-1-minimal/pyproject.toml | 47 +- examples/k8s-1-minimal/requirements.txt | 1 - examples/k8s-1-minimal/src/charm.py | 43 +- .../tests/integration/conftest.py | 42 +- .../tests/integration/test_charm.py | 20 +- .../k8s-1-minimal/tests/unit/test_charm.py | 20 +- examples/k8s-1-minimal/tox.ini | 75 ++- examples/k8s-1-minimal/uv.lock | 433 ++++++++++++++++++ 10 files changed, 629 insertions(+), 119 deletions(-) delete mode 100644 examples/k8s-1-minimal/requirements.txt create mode 100644 examples/k8s-1-minimal/uv.lock diff --git a/.github/workflows/example-charm-integration-tests.yaml b/.github/workflows/example-charm-integration-tests.yaml index 0c5d94da6..8862b01c3 100644 --- a/.github/workflows/example-charm-integration-tests.yaml +++ b/.github/workflows/example-charm-integration-tests.yaml @@ -45,6 +45,7 @@ jobs: matrix: dir: - examples/httpbin-demo + - examples/k8s-1-minimal steps: - uses: actions/checkout@v6 with: @@ -78,7 +79,6 @@ jobs: fail-fast: false matrix: dir: - - examples/k8s-1-minimal - examples/k8s-2-configurable - examples/k8s-3-postgresql - examples/k8s-4-action diff --git a/examples/k8s-1-minimal/charmcraft.yaml b/examples/k8s-1-minimal/charmcraft.yaml index 5b26da883..e72f2e0cd 100644 --- a/examples/k8s-1-minimal/charmcraft.yaml +++ b/examples/k8s-1-minimal/charmcraft.yaml @@ -1,35 +1,56 @@ +# This file configures Charmcraft. +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ type: charm -bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" - -name: demo-api-charm -title: | - demo-fastapi-k8s +name: fastapi-demo +title: Web Server Demo +summary: A demo charm that operates a small Python FastAPI server. description: | - This is a demo charm built on top of a small Python FastAPI server. -summary: | - FastAPI Demo charm for Kubernetes + This charm demonstrates how to write a Kubernetes charm with Ops. + +# Documentation: +# https://documentation.ubuntu.com/charmcraft/stable/howto/build-guides/select-platforms/ +base: ubuntu@22.04 +platforms: + amd64: + arm64: + +parts: + charm: + plugin: uv + source: . + build-snaps: + - astral-uv + +# (Optional) Configuration options for the charm +# This config section defines charm config options, and populates the Configure +# tab on Charmhub. +# More information on this section at: +# https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/#config +# General configuration documentation: +# https://documentation.ubuntu.com/juju/3.6/reference/configuration/#application-configuration +config: + options: + # An example config option to customise the log level of the workload + log-level: + description: | + Configures the log level of gunicorn. -assumes: - - juju >= 3.1 - - k8s-api + Acceptable values are: "info", "debug", "warning", "error" and "critical" + default: "info" + type: string containers: demo-server: resource: demo-server-image resources: - # An OCI image resource for each container listed above. - # You may remove this if your charm will run without a workload sidecar container. + # An OCI image resource for the container listed above. demo-server-image: type: oci-image description: OCI image from GitHub Container Repository - # The upstream-source field is ignored by Juju. It is included here as a reference - # so the integration testing suite knows which image to deploy during testing. This field - # is also used by the 'canonical/charming-actions' GitHub action for automated releasing. + # The upstream-source field is ignored by Charmcraft and Juju, but it can be + # useful to developers in identifying the source of the OCI image. It is also + # used by the 'canonical/charming-actions' GitHub action for automated releases. + # The test_deploy function in tests/integration/test_charm.py reads upstream-source + # to determine which OCI image to use when running the charm's integration tests. upstream-source: ghcr.io/canonical/api_demo_server:1.0.1 diff --git a/examples/k8s-1-minimal/pyproject.toml b/examples/k8s-1-minimal/pyproject.toml index 9235e5544..3b8726f1d 100644 --- a/examples/k8s-1-minimal/pyproject.toml +++ b/examples/k8s-1-minimal/pyproject.toml @@ -1,3 +1,49 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[project] +name = "fastapi-demo" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should include the dependencies of the code in src/. You should also include the +# dependencies of any charmlibs that the charm uses (copy the dependencies from PYDEPS). +dependencies = [ + "ops>=3,<4", +] + +[dependency-groups] +# Dependencies of linting and static type checks +lint = [ + "ruff", + "codespell", + "pyright", +] +# Dependencies of unit tests +unit = [ + "coverage[toml]", + "ops[testing]", + "pytest", +] +# Dependencies of integration tests +integration = [ + "jubilant", + "pytest", + "PyYAML", +] + # Testing tools configuration [tool.coverage.run] branch = true @@ -11,7 +57,6 @@ log_cli_level = "INFO" # Linting tools configuration [tool.ruff] -format.quote-style = "single" # Remove this when switching the tutorial to charmcraft init. line-length = 99 lint.select = ["E", "W", "F", "C", "N", "D", "I001"] lint.ignore = [ diff --git a/examples/k8s-1-minimal/requirements.txt b/examples/k8s-1-minimal/requirements.txt deleted file mode 100644 index ba15e9e79..000000000 --- a/examples/k8s-1-minimal/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -ops>2,<4 diff --git a/examples/k8s-1-minimal/src/charm.py b/examples/k8s-1-minimal/src/charm.py index c39cd7934..82b196ba1 100755 --- a/examples/k8s-1-minimal/src/charm.py +++ b/examples/k8s-1-minimal/src/charm.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -31,24 +31,15 @@ class FastAPIDemoCharm(ops.CharmBase): def __init__(self, framework: ops.Framework) -> None: super().__init__(framework) - self.pebble_service_name = 'fastapi-service' + self.pebble_service_name = "fastapi-service" framework.observe(self.on.demo_server_pebble_ready, self._on_demo_server_pebble_ready) def _on_demo_server_pebble_ready(self, event: ops.PebbleReadyEvent) -> None: - """Define and start a workload using the Pebble API. - - Change this example to suit your needs. You'll need to specify the right entrypoint and - environment configuration for your specific workload. - - Learn more about interacting with Pebble at - https://documentation.ubuntu.com/ops/latest/reference/pebble/ - Learn more about Pebble layers at - https://documentation.ubuntu.com/pebble/how-to/use-layers/ - """ + """Define and start a workload using the Pebble API.""" # Get a reference the container attribute on the PebbleReadyEvent container = event.workload # Add initial Pebble config layer using the Pebble API - container.add_layer('fastapi_demo', self._get_pebble_layer(), combine=True) + container.add_layer("fastapi_demo", self._get_pebble_layer(), combine=True) # Make Pebble reevaluate its plan, ensuring any services are started if enabled. container.replan() # Learn more about statuses at @@ -57,28 +48,28 @@ def _on_demo_server_pebble_ready(self, event: ops.PebbleReadyEvent) -> None: def _get_pebble_layer(self) -> ops.pebble.Layer: """Pebble layer for the FastAPI demo services.""" - command = ' '.join( + command = " ".join( [ - 'uvicorn', - 'api_demo_server.app:app', - '--host=0.0.0.0', - '--port=8000', + "uvicorn", + "api_demo_server.app:app", + "--host=0.0.0.0", + "--port=8000", ] ) pebble_layer: ops.pebble.LayerDict = { - 'summary': 'FastAPI demo service', - 'description': 'pebble config layer for FastAPI demo server', - 'services': { + "summary": "FastAPI demo service", + "description": "pebble config layer for FastAPI demo server", + "services": { self.pebble_service_name: { - 'override': 'replace', - 'summary': 'fastapi demo', - 'command': command, - 'startup': 'enabled', + "override": "replace", + "summary": "fastapi demo", + "command": command, + "startup": "enabled", } }, } return ops.pebble.Layer(pebble_layer) -if __name__ == '__main__': # pragma: nocover +if __name__ == "__main__": # pragma: nocover ops.main(FastAPIDemoCharm) diff --git a/examples/k8s-1-minimal/tests/integration/conftest.py b/examples/k8s-1-minimal/tests/integration/conftest.py index ce18dd2f2..db029daad 100644 --- a/examples/k8s-1-minimal/tests/integration/conftest.py +++ b/examples/k8s-1-minimal/tests/integration/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,26 +11,48 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# +# The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ +import logging +import os import pathlib -import subprocess -from typing import Iterator +import sys +import time import jubilant import pytest +logger = logging.getLogger(__name__) + -@pytest.fixture(scope='module') -def juju(request: pytest.FixtureRequest) -> Iterator[jubilant.Juju]: +@pytest.fixture(scope="module") +def juju(request: pytest.FixtureRequest): + """Create a temporary Juju model for running tests.""" with jubilant.temp_model() as juju: yield juju if request.session.testsfailed: + logger.info("Collecting Juju logs...") + time.sleep(0.5) # Wait for Juju to process logs. log = juju.debug_log(limit=1000) - print(log, end='') + print(log, end="", file=sys.stderr) -@pytest.fixture(scope='session') -def charm() -> pathlib.Path: - subprocess.check_call(['charmcraft', 'pack']) # noqa - return next(pathlib.Path('.').glob('*.charm')) +@pytest.fixture(scope="session") +def charm(): + """Return the path of the charm under test.""" + if "CHARM_PATH" in os.environ: + charm_path = pathlib.Path(os.environ["CHARM_PATH"]) + if not charm_path.exists(): + raise FileNotFoundError(f"Charm does not exist: {charm_path}") + return charm_path + # Modify below if you're building for multiple bases or architectures. + charm_paths = list(pathlib.Path(".").glob("*.charm")) + if not charm_paths: + raise FileNotFoundError("No .charm file in current directory") + if len(charm_paths) > 1: + path_list = ", ".join(str(path) for path in charm_paths) + raise ValueError(f"More than one .charm file in current directory: {path_list}") + return charm_paths[0] diff --git a/examples/k8s-1-minimal/tests/integration/test_charm.py b/examples/k8s-1-minimal/tests/integration/test_charm.py index 6a998c3bc..0e3c8c493 100644 --- a/examples/k8s-1-minimal/tests/integration/test_charm.py +++ b/examples/k8s-1-minimal/tests/integration/test_charm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,6 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# +# The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ import logging import pathlib @@ -20,19 +23,14 @@ logger = logging.getLogger(__name__) -METADATA = yaml.safe_load(pathlib.Path('./charmcraft.yaml').read_text()) -APP_NAME = METADATA['name'] +METADATA = yaml.safe_load(pathlib.Path("charmcraft.yaml").read_text()) +APP_NAME = METADATA["name"] def test_deploy(charm: pathlib.Path, juju: jubilant.Juju): - """Deploy the charm under test. - - Assert on the unit status before any relations/configurations take place. - """ + """Deploy the charm under test.""" resources = { - 'demo-server-image': METADATA['resources']['demo-server-image']['upstream-source'] + "demo-server-image": METADATA["resources"]["demo-server-image"]["upstream-source"] } - - # Deploy the charm and wait for active/idle status - juju.deploy(f'./{charm}', app=APP_NAME, resources=resources) + juju.deploy(charm.resolve(), app=APP_NAME, resources=resources) juju.wait(jubilant.all_active) diff --git a/examples/k8s-1-minimal/tests/unit/test_charm.py b/examples/k8s-1-minimal/tests/unit/test_charm.py index 23fb2288a..03223ee54 100644 --- a/examples/k8s-1-minimal/tests/unit/test_charm.py +++ b/examples/k8s-1-minimal/tests/unit/test_charm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,6 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# +# To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ import ops from ops import testing @@ -20,7 +22,7 @@ def test_pebble_layer(): ctx = testing.Context(FastAPIDemoCharm) - container = testing.Container(name='demo-server', can_connect=True) + container = testing.Container(name="demo-server", can_connect=True) state_in = testing.State( containers={container}, leader=True, @@ -28,12 +30,12 @@ def test_pebble_layer(): state_out = ctx.run(ctx.on.pebble_ready(container), state_in) # Expected plan after Pebble ready with default config expected_plan = { - 'services': { - 'fastapi-service': { - 'override': 'replace', - 'summary': 'fastapi demo', - 'command': 'uvicorn api_demo_server.app:app --host=0.0.0.0 --port=8000', - 'startup': 'enabled', + "services": { + "fastapi-service": { + "override": "replace", + "summary": "fastapi demo", + "command": "uvicorn api_demo_server.app:app --host=0.0.0.0 --port=8000", + "startup": "enabled", # Since the environment is empty, Layer.to_dict() will not # include it. } @@ -46,6 +48,6 @@ def test_pebble_layer(): assert state_out.unit_status == testing.ActiveStatus() # Check the service was started: assert ( - state_out.get_container(container.name).service_statuses['fastapi-service'] + state_out.get_container(container.name).service_statuses["fastapi-service"] == ops.pebble.ServiceStatus.ACTIVE ) diff --git a/examples/k8s-1-minimal/tox.ini b/examples/k8s-1-minimal/tox.ini index b31c45310..0b13726a6 100644 --- a/examples/k8s-1-minimal/tox.ini +++ b/examples/k8s-1-minimal/tox.ini @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ [tox] no_package = True skip_missing_interpreters = True +env_list = format, lint, unit min_version = 4.0.0 -env_list = unit [vars] src_path = {tox_root}/src tests_path = {tox_root}/tests +;lib_path = {tox_root}/lib/charms/operator_name_with_underscores all_path = {[vars]src_path} {[vars]tests_path} [testenv] @@ -33,14 +34,35 @@ pass_env = CHARM_BUILD_DIR MODEL_SETTINGS +[testenv:format] +description = Apply coding style standards to code +deps = + ruff +commands = + ruff format {[vars]all_path} + ruff check --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards, and static checks +runner = uv-venv-lock-runner +dependency_groups = + lint + unit + integration +commands = + # if this charm owns a lib, uncomment "lib_path" variable + # and uncomment the following line + # codespell {[vars]lib_path} + codespell {tox_root} + ruff check {[vars]all_path} + ruff format --check --diff {[vars]all_path} + pyright {posargs} + [testenv:unit] description = Run unit tests -deps = - pytest - cosl - coverage[toml] - ops[testing] - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + unit commands = coverage run --source={[vars]src_path} -m pytest \ -v \ @@ -52,10 +74,13 @@ commands = [testenv:integration] description = Run integration tests -deps = - pytest - jubilant - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + integration +pass_env = + # The integration tests don't pack the charm. If CHARM_PATH is set, the tests deploy the + # specified .charm file. Otherwise, the tests look for a .charm file in the project dir. + CHARM_PATH commands = pytest \ -v \ @@ -64,29 +89,3 @@ commands = --log-cli-level=INFO \ {[vars]tests_path}/integration \ {posargs} - -# Formatting and linting environments. These aren't currently covered by the tutorial. - -[testenv:format] -description = Apply coding style standards to code -deps = - ruff -commands = - ruff format {[vars]all_path} - ruff check --fix {[vars]all_path} - -[testenv:lint] -description = Run linting and type checks -deps = - ruff - codespell - pyright - ops[testing] - -r {tox_root}/requirements.txt - pytest - jubilant -commands = - codespell {tox_root} - ruff check {[vars]all_path} - ruff format --check --diff {[vars]all_path} - pyright {posargs} diff --git a/examples/k8s-1-minimal/uv.lock b/examples/k8s-1-minimal/uv.lock new file mode 100644 index 000000000..cf228ef03 --- /dev/null +++ b/examples/k8s-1-minimal/uv.lock @@ -0,0 +1,433 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "codespell" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/78/1c1c5ec58f16817c09cbacb39783c3655d54a221b6552f47ff5ac9297603/coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", size = 212028, upload-time = "2025-06-13T13:00:29.293Z" }, + { url = "https://files.pythonhosted.org/packages/98/db/e91b9076f3a888e3b4ad7972ea3842297a52cc52e73fd1e529856e473510/coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", size = 212420, upload-time = "2025-06-13T13:00:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d0/2b3733412954576b0aea0a16c3b6b8fbe95eb975d8bfa10b07359ead4252/coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", size = 241529, upload-time = "2025-06-13T13:00:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/b3/00/5e2e5ae2e750a872226a68e984d4d3f3563cb01d1afb449a17aa819bc2c4/coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3", size = 239403, upload-time = "2025-06-13T13:00:37.399Z" }, + { url = "https://files.pythonhosted.org/packages/37/3b/a2c27736035156b0a7c20683afe7df498480c0dfdf503b8c878a21b6d7fb/coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", size = 240548, upload-time = "2025-06-13T13:00:39.647Z" }, + { url = "https://files.pythonhosted.org/packages/98/f5/13d5fc074c3c0e0dc80422d9535814abf190f1254d7c3451590dc4f8b18c/coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", size = 240459, upload-time = "2025-06-13T13:00:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/36/24/24b9676ea06102df824c4a56ffd13dc9da7904478db519efa877d16527d5/coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", size = 239128, upload-time = "2025-06-13T13:00:42.343Z" }, + { url = "https://files.pythonhosted.org/packages/be/05/242b7a7d491b369ac5fee7908a6e5ba42b3030450f3ad62c645b40c23e0e/coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", size = 239402, upload-time = "2025-06-13T13:00:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/73/e0/4de7f87192fa65c9c8fbaeb75507e124f82396b71de1797da5602898be32/coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", size = 214518, upload-time = "2025-06-13T13:00:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/5e4e2fe458907d2a65fab62c773671cfc5ac704f1e7a9ddd91996f66e3c2/coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", size = 215436, upload-time = "2025-06-13T13:00:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, + { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, + { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, + { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, + { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, + { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, + { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, + { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, + { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload-time = "2025-06-13T13:01:35.285Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload-time = "2025-06-13T13:01:36.712Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload-time = "2025-06-13T13:01:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload-time = "2025-06-13T13:01:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload-time = "2025-06-13T13:01:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload-time = "2025-06-13T13:01:44.482Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload-time = "2025-06-13T13:01:45.772Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload-time = "2025-06-13T13:01:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload-time = "2025-06-13T13:01:48.554Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload-time = "2025-06-13T13:01:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload-time = "2025-06-13T13:01:51.314Z" }, + { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload-time = "2025-06-13T13:01:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload-time = "2025-06-13T13:01:56.769Z" }, + { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload-time = "2025-06-13T13:01:58.19Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload-time = "2025-06-13T13:01:59.645Z" }, + { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload-time = "2025-06-13T13:02:01.37Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, + { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, + { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jubilant" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/49/9ea5efac9127c76247d42e286e56e26d9b5c01edbf9f24bcfae9aab3cf81/jubilant-1.3.0.tar.gz", hash = "sha256:ff43d6eb67a986958db6317d7ff3df1c8c160d0c56736628919ac1f7319d444e", size = 26842, upload-time = "2025-07-24T22:31:55.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/97/ad9cbc4718cdc4feed0e841ccb2a3d15de7cb1187d63d1e2ba419cc34f51/jubilant-1.3.0-py3-none-any.whl", hash = "sha256:a5ea4a3bf487ab0286eaad0de9df145761657c08beb834931340b9ebb1f41292", size = 26484, upload-time = "2025-07-24T22:31:54.467Z" }, +] + +[[package]] +name = "fastapi-demo" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "ops" }, +] + +[package.dev-dependencies] +integration = [ + { name = "jubilant" }, + { name = "pytest" }, + { name = "pyyaml" }, +] +lint = [ + { name = "codespell" }, + { name = "pyright" }, + { name = "ruff" }, +] +unit = [ + { name = "coverage", extra = ["toml"] }, + { name = "ops", extra = ["testing"] }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "ops", specifier = ">=3,<4" }] + +[package.metadata.requires-dev] +integration = [ + { name = "jubilant" }, + { name = "pytest" }, + { name = "pyyaml" }, +] +lint = [ + { name = "codespell" }, + { name = "pyright" }, + { name = "ruff" }, +] +unit = [ + { name = "coverage", extras = ["toml"] }, + { name = "ops", extras = ["testing"] }, + { name = "pytest" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" }, +] + +[[package]] +name = "ops" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "opentelemetry-api" }, + { name = "pyyaml" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/92/6bf6c91d4d0e4007d835fe73b1b73159d0c3dde4059c301c572ce2fc3ddd/ops-3.0.0.tar.gz", hash = "sha256:f4709f7699a2b8b0aaa3a6ad891fb8e4925792121fcb762ef264d24f87675680", size = 527591, upload-time = "2025-07-02T10:40:20.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/d6/fe94db94ab773efa87223ffbf1f3c243e04c37d82b366c6a900c1ba1ea0e/ops-3.0.0-py3-none-any.whl", hash = "sha256:f71d62c1d5ae58c01acc37fb330c6aa13aa1e2913fc44519b5ac31b93b9181ec", size = 188167, upload-time = "2025-07-02T10:40:18.439Z" }, +] + +[package.optional-dependencies] +testing = [ + { name = "ops-scenario" }, +] + +[[package]] +name = "ops-scenario" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ops" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/23/3737c3a76bd3129eb571538d22e81f58727c0670b6cba24febd4ccdeb2f0/ops_scenario-8.0.0.tar.gz", hash = "sha256:b358228f3c88ab36f93c467e926c2ef20f16a99578a490a9fd9733eb1eab175c", size = 109454, upload-time = "2025-07-02T10:40:32.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c7/78f623df92e3c6220eb8dd963f7fbb3df69934c74aa073deb7c77d4407e3/ops_scenario-8.0.0-py3-none-any.whl", hash = "sha256:5d52e7162cc7cb2a26c8fd62df3f3fb88668a407af4024c1be6003959c80e2de", size = 64300, upload-time = "2025-07-02T10:40:31.002Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.402" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207, upload-time = "2025-06-11T08:48:35.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004, upload-time = "2025-06-11T08:48:33.998Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]