diff --git a/docs/api/requests.md b/docs/api/requests.md index 3fd2ef5eb..2197969f1 100644 --- a/docs/api/requests.md +++ b/docs/api/requests.md @@ -61,6 +61,7 @@ async def create_item(request: Request): ### Reading Form Data + ```python import air from air.requests import Request @@ -76,6 +77,7 @@ async def login(request: Request): air.Section(air.Aside({"username": form.get("username")})) ) ``` + ### Accessing the HTMX object diff --git a/docs/api/routing.md b/docs/api/routing.md index 414f19fe5..9977beace 100644 --- a/docs/api/routing.md +++ b/docs/api/routing.md @@ -4,6 +4,7 @@ If you need to knit several Python modules with their own Air views into one, th Let's imagine we have an e-commerce store with a shopping cart app. Use instantiate a `router` object using `air.AirRouter()` just as we would with `air.App()`: + ```python # cart.py import air @@ -15,9 +16,11 @@ router = air.AirRouter() def cart(): return air.H1("I am a shopping cart") ``` + Then in our main page we can load that and tie it into our main `app`. + ```python import air from cart import router as cart_router @@ -30,11 +33,13 @@ app.include_router(cart_router) def index(): return air.H1("Home page") ``` + Note that the router allows sharing of sessions and other application states. In addition, we can add links through the `.url()` method available on route functions, which generates URLs programmatically: + ```python import air from cart import router as cart_router, cart @@ -47,11 +52,13 @@ app.include_router(cart_router) def index(): return air.Div(air.H1("Home page"), air.A("View cart", href=cart.url())) ``` + ## Query Parameters Air supports query parameters through FastAPI's `Query()` validator, which you can import as `air.Query()`: + ```python import air @@ -70,9 +77,11 @@ def index(): air.A("Search", href=search.url(query_params={"q": "air", "page": 1})) ) ``` + The `.url()` method accepts a `query_params` argument for generating URLs with query strings. This works with both scalar values and lists: + ```python @app.get("/filter") def filter_items( @@ -84,6 +93,7 @@ def filter_items( # Generates: /filter?tags=python&tags=web url = filter_items.url(query_params={"tags": ["python", "web"]}) ``` + --- diff --git a/docs/learn/airmodel.md b/docs/learn/airmodel.md index 69b9277d5..0875cc0b2 100644 --- a/docs/learn/airmodel.md +++ b/docs/learn/airmodel.md @@ -213,10 +213,12 @@ Insert, update, or delete hundreds of rows in a single SQL statement: ```python # Insert multiple rows at once -fruits = await DragonFruit.bulk_create([ - {"name": "Pink Pitaya", "color": "magenta"}, - {"name": "Yellow Dragon", "color": "yellow"}, -]) +fruits = await DragonFruit.bulk_create( + [ + {"name": "Pink Pitaya", "color": "magenta"}, + {"name": "Yellow Dragon", "color": "yellow"}, + ] +) # Update all matching rows count = await DragonFruit.bulk_update({"color": "red"}, name__contains="Dragon") @@ -274,6 +276,7 @@ async def submit_contact(request: air.Request): ## A complete app in 30 lines + ```python title="main.py" import air from air import AirForm @@ -318,6 +321,7 @@ async def sign(request: air.Request): await GuestBookEntry.create(name=form.data.name, message=form.data.message) return air.RedirectResponse("/") ``` + Model, form, database, HTML, validation, pagination, and two routes. Set `DATABASE_URL` and run it. diff --git a/docs/learn/cookbook/bigger-applications.md b/docs/learn/cookbook/bigger-applications.md index 6f1a9fcdd..cb0dffab3 100644 --- a/docs/learn/cookbook/bigger-applications.md +++ b/docs/learn/cookbook/bigger-applications.md @@ -10,6 +10,7 @@ When building larger applications with Air, you may find yourself needing to org Let's imagine we have a landing page that links to a sophisticated dashboard. While our example dashboard is trivial, let's assume it is complicated enough that we want it in a separate Python module yet share state. We design the `main.py` as we would a normal Air application: + ```python title="main.py" import air @@ -20,9 +21,11 @@ app = air.Air() def index(): return air.layouts.mvpcss(air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard"))) ``` + Now for the dashboard, instead of using the typical `air.Air` tool to instantiate our application, we use `air.AirRouter` like so: + ```python title="dashboard.py" hl_lines="3" import air @@ -33,9 +36,11 @@ router = air.AirRouter() def dashboard(): return air.layouts.mvpcss(air.H1("Avatar Data Dashboard"), air.P(air.A("<- Home", href="/"))) ``` + Now if we go back to our `main.py` we can use the `app.include_router()` method to include the dashboard in our app: + ```python title="main.py" hl_lines="2 5" import air from .dashboard import router @@ -50,6 +55,7 @@ def index(): air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard")) ) ``` + If run locally these links should work: @@ -64,6 +70,7 @@ If run locally these links should work: One of the really nice features of Air is the ability to mount apps inside each other. This allows you to create modular applications where different parts of your app can be developed and maintained independently. To do this, we lean on Starlette's `mount` functionality that Air inherits through FastAPI. + ```python import air @@ -90,11 +97,13 @@ def index(): # This allows you to access the shop at /shop app.mount("/shop", shop) ``` + ## Mounting FastAPI inside of Air apps You can easily mount a FastAPI app inside an Air app. A common scenario is to have a FastAPI app that serves an API, while your main Air app serves the landing, billing, and usage frontends. + ```python from fastapi import FastAPI @@ -126,6 +135,7 @@ def api_root(): # Combining the Air and and FastAPI apps into one app.mount("/api", api) ``` + ## Mounting FastAPI apps inside each other diff --git a/docs/learn/cookbook/charts.md b/docs/learn/cookbook/charts.md index 52127289c..9f4c61977 100644 --- a/docs/learn/cookbook/charts.md +++ b/docs/learn/cookbook/charts.md @@ -2,6 +2,7 @@ Air is great for building charts. Using [plotly](https://plotly.com/javascript/), here's a simple chart example. + ```python import json @@ -13,23 +14,25 @@ app = air.Air() @app.get("/") def index(): title = "Air Chart Demo" - data = json.dumps({ - "data": [ - { - "x": [0, 4, 5, 7, 8, 10], - "y": [2, 9, 0, 4, 3, 6], - "type": "scatter", - }, - { - "x": [0, 1, 2, 4, 8, 10], - "y": [9, 2, 4, 3, 5, 0], - "type": "scatter", - }, - ], - "title": "Fun charts with Plotly and Air", - "description": ("This is a demonstration of how to build a chart using Plotly and Air"), - "type": "scatter", - }) + data = json.dumps( + { + "data": [ + { + "x": [0, 4, 5, 7, 8, 10], + "y": [2, 9, 0, 4, 3, 6], + "type": "scatter", + }, + { + "x": [0, 1, 2, 4, 8, 10], + "y": [9, 2, 4, 3, 5, 0], + "type": "scatter", + }, + ], + "title": "Fun charts with Plotly and Air", + "description": ("This is a demonstration of how to build a chart using Plotly and Air"), + "type": "scatter", + } + ) return air.layouts.mvpcss( air.Script(src="https://cdn.plot.ly/plotly-3.0.1.min.js"), air.Title(title), @@ -46,9 +49,11 @@ def index(): ), ) ``` + Air makes it possible to build charts that pull data from servers and animate the results. Here's an example being supplied with random numbers for the Air server. + ```python import air @@ -69,3 +74,4 @@ air.Children( ), ) ``` + diff --git a/docs/learn/cookbook/forms.md b/docs/learn/cookbook/forms.md index 196f1b83b..ea604dd50 100644 --- a/docs/learn/cookbook/forms.md +++ b/docs/learn/cookbook/forms.md @@ -2,6 +2,7 @@ Built on Pydantic's `BaseModel`, the `AirForm` class validates data from HTML forms with type-safe access to the validated result. + ```python from airmodel import AirModel @@ -54,6 +55,7 @@ async def flight_info(request: air.Request): ), ) ``` + ## Enhanced Form Features diff --git a/docs/learn/def_vs_async_def.md b/docs/learn/def_vs_async_def.md index 68d1b0688..8e649196f 100644 --- a/docs/learn/def_vs_async_def.md +++ b/docs/learn/def_vs_async_def.md @@ -19,10 +19,12 @@ def user_detail(user_id: int): user = db.get_user(user_id) return air.H1(user.name) + @app.get("/about") def about(): return air.H1("About") + @app.get("/data") def get_data(): content = open("data.csv").read() @@ -41,6 +43,7 @@ async def handle_form(request: air.Request): return air.H1(f"Hello, {form.data.name}!") return form.render() + @app.get("/external") async def fetch_data(): async with httpx.AsyncClient() as client: @@ -57,6 +60,7 @@ async def fetch_data(): async def get_data(): return open("big_file.csv").read() + # RIGHT: runs in a thread, other requests keep flowing @app.get("/data") def get_data(): diff --git a/docs/learn/forms.md b/docs/learn/forms.md index 896d597ad..402da6e5a 100644 --- a/docs/learn/forms.md +++ b/docs/learn/forms.md @@ -152,9 +152,9 @@ After validation, `form.data` is the model instance with full type information: ```python if form.is_valid: - form.data.route_name # your editor knows this is a str - form.data.destination # autocomplete works - form.data.orign # typo caught by the type checker + form.data.route_name # your editor knows this is a str + form.data.destination # autocomplete works + form.data.orign # typo caught by the type checker ``` In Django, `form.cleaned_data["route_name"]` is an untyped dict access. Typos become runtime bugs. In WTForms, `form.route_name.data` has no type information. Air is the first Python form system where your editor knows the shape of validated data, because `form.data` is the actual Pydantic model. @@ -241,10 +241,11 @@ For context-aware visibility, use `Annotated` with AirField metadata types direc from typing import Annotated from airfield import Hidden, ReadOnly + class ArticleModel(AirModel): title: str - slug: Annotated[str, Hidden("form")] # hidden in forms, visible in tables - internal: Annotated[str, ReadOnly("form")] # read-only in forms + slug: Annotated[str, Hidden("form")] # hidden in forms, visible in tables + internal: Annotated[str, ReadOnly("form")] # read-only in forms ``` Pydantic constraints like `min_length` and `max_length` automatically become HTML5 `minlength` and `maxlength` attributes, so browser-side validation matches server-side rules. @@ -267,9 +268,9 @@ Use `excludes` to hide fields from display, saving, or both: ```python class JeepneyRouteForm(AirForm[JeepneyRouteModel]): excludes = ( - "internal_id", # hidden from display and save - ("origin", "display"), # not rendered, still in save_data() - ("tracking_code", "save"), # rendered, excluded from save_data() + "internal_id", # hidden from display and save + ("origin", "display"), # not rendered, still in save_data() + ("tracking_code", "save"), # rendered, excluded from save_data() ) ``` diff --git a/docs/learn/layouts.md b/docs/learn/layouts.md index a19fcc009..c63add326 100644 --- a/docs/learn/layouts.md +++ b/docs/learn/layouts.md @@ -9,6 +9,7 @@ Layouts in Air provide a way to structure complete HTML documents without the re Air's layout functions automatically sort your tags into the right places using intelligent filtering. This allows you eliminate repetitive `air.Html`, `air.Body`, and `air.Head` boilerplate. + ```python # Verbose Way air.Html( @@ -26,6 +27,7 @@ air.layouts.mvpcss( air.P("Content here"), # Also goes to ) ``` + ## The Tag Filtering System @@ -38,6 +40,7 @@ Air layouts use two core functions to organize content: This automatic separation means you can focus on your content and let Air handle the document structure: + ```python @app.get("/") def home(): @@ -50,6 +53,7 @@ def home(): air.Script(src="dashboard.js"), ) ``` + Air transforms this into proper HTML structure automatically. @@ -57,6 +61,7 @@ Air transforms this into proper HTML structure automatically. Air provides minimal ready-to-use layouts for rapid prototyping, `mvpcss` and `picocss` for MVP.css and PicoCSS respectively. They both work and are used in the exact same way. + ```python import air @@ -70,6 +75,7 @@ def home(): air.Button("Get Started"), ) ``` + **What you get:** @@ -90,6 +96,7 @@ The included layouts are designed for **quick prototyping**, not production comm Here's the foundational pattern for any Air layout: + ```python import air @@ -114,6 +121,7 @@ def my_layout(*children, **kwargs): ), ).render() ``` + **Key principles:** diff --git a/justfile b/justfile index 299fd3499..e1a4a8087 100644 --- a/justfile +++ b/justfile @@ -29,6 +29,10 @@ fix: uv run ruff format . uv run ruff check --fix . +# Format Python code blocks in documentation files +fmt-docs: + git ls-files -z -- 'docs/**/*.md' '*.md' | xargs -0 uv run blacken-docs --line-length 120 + # Tag, push, and create a GitHub release release: uv run scripts/release.py diff --git a/pyproject.toml b/pyproject.toml index e00c4c7a7..0436f8b97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,7 @@ examples = [ "pyinstrument>=5.1.2", # https://pypi.org/project/pyinstrument ] devtools = [ + "blacken-docs>=1.20.0", # https://pypi.org/project/blacken-docs "ruff>=0.15.0", # https://pypi.org/project/ruff "ty>=0.0.19", # https://pypi.org/project/ty ] diff --git a/uv.lock b/uv.lock index bff35c0c5..f436e520c 100644 --- a/uv.lock +++ b/uv.lock @@ -33,6 +33,7 @@ standard = [ [package.dev-dependencies] dev = [ + { name = "blacken-docs" }, { name = "click" }, { name = "coverage" }, { name = "full-match" }, @@ -59,6 +60,7 @@ dev = [ { name = "types-pygments" }, ] devtools = [ + { name = "blacken-docs" }, { name = "ruff" }, { name = "ty" }, ] @@ -118,6 +120,7 @@ provides-extras = ["standard"] [package.metadata.requires-dev] dev = [ + { name = "blacken-docs", specifier = ">=1.20.0" }, { name = "click", specifier = ">=8.3.1" }, { name = "coverage", extras = ["toml"], specifier = ">=7.13.4" }, { name = "full-match", specifier = ">=0.0.3" }, @@ -144,6 +147,7 @@ dev = [ { name = "types-pygments", specifier = ">=2.19.0.20251121" }, ] devtools = [ + { name = "blacken-docs", specifier = ">=1.20.0" }, { name = "ruff", specifier = ">=0.15.0" }, { name = "ty", specifier = ">=0.0.19" }, ] @@ -346,6 +350,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + +[[package]] +name = "blacken-docs" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "black" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/bc/c8fbeb90232c3afe0b369de4014c50925a5b3e6fcba5a784d96377da3d09/blacken_docs-1.20.0.tar.gz", hash = "sha256:2d5b6caf6e7da5694b1eba97f9132c1ab9f14f221c82205ec473a6e74fbb2c6d", size = 14988, upload-time = "2025-09-08T15:33:18.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e7/63af39c07bbb019feb1e4f89638410ec08101b59f094c51eb6bd5667b9e9/blacken_docs-1.20.0-py3-none-any.whl", hash = "sha256:a0d842811ee07802dec920d3cf831e21f6eb017712748b488489aa3688770f1e", size = 8324, upload-time = "2025-09-08T15:33:17.135Z" }, +] + [[package]] name = "bleach" version = "6.3.0" @@ -836,6 +879,7 @@ wheels = [ name = "griffelib" version = "2.0.0" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] @@ -1512,6 +1556,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "nbclient" version = "0.10.4" @@ -2068,6 +2121,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3"