Compare commits
36 commits
Author | SHA1 | Date | |
---|---|---|---|
e2064ddba7 | |||
1a35ca0ed8 | |||
66a5f81230 | |||
b0e69727e2 | |||
ced11ac2da | |||
223c7f3cc6 | |||
3caf7cdb0b | |||
1a4aafd773 | |||
81a8cd1ee7 | |||
96f5748565 | |||
24ee9bdda2 | |||
0cd1bed62c | |||
7740299ad8 | |||
991d6be5a3 | |||
557cd39c13 | |||
ff8f50e44b | |||
f4ffa14b00 | |||
b9ad2467df | |||
972225d8bc | |||
95f9479aa9 | |||
707a3a82c3 | |||
dca1b89b39 | |||
98ca09b681 | |||
6c2cfc5297 | |||
203b4f7e0f | |||
236fc84be1 | |||
b14db9a0ae | |||
![]() |
f1d1aebc96 | ||
![]() |
491f6c3c3a | ||
f94f0f5134 | |||
7ac76883fc | |||
12f6c726c9 | |||
3d24b53fdf | |||
9a407495f8 | |||
61d30dca23 | |||
fd6df38cdf |
31 changed files with 1223 additions and 642 deletions
17
README.md
17
README.md
|
@ -22,7 +22,9 @@ So I built one.
|
||||||
`spiderweb` is a small web framework, just big enough to hold a spider. Getting started is easy:
|
`spiderweb` is a small web framework, just big enough to hold a spider. Getting started is easy:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
poetry add spiderweb-framework
|
uv add spiderweb-framework
|
||||||
|
# or
|
||||||
|
pip install spiderweb-framework
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a new file and drop this in it:
|
Create a new file and drop this in it:
|
||||||
|
@ -43,6 +45,17 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
## [View the docs here!](https://itsthejoker.github.io/spiderweb/#/)
|
## [View the docs here!](https://itsthejoker.github.io/spiderweb/#/)
|
||||||
|
|
||||||
|
### Development (using uv)
|
||||||
|
|
||||||
|
This repository uses uv for local development and testing.
|
||||||
|
|
||||||
|
- Create a virtual environment: `uv venv`
|
||||||
|
- Activate it (Windows): `.venv\Scripts\activate`
|
||||||
|
- Activate it (POSIX): `source .venv/bin/activate`
|
||||||
|
- Install deps (editable + dev): `uv pip install -e .[dev]`
|
||||||
|
- Run tests: `uv run python -m pytest`
|
||||||
|
- Lint/format: `uv run ruff check .` and `uv run black .`
|
||||||
|
|
||||||
My goal with this framework was to do three things:
|
My goal with this framework was to do three things:
|
||||||
|
|
||||||
1. Learn a lot
|
1. Learn a lot
|
||||||
|
@ -68,5 +81,5 @@ And, honestly, I think I got there. Here's a non-exhaustive list of things this
|
||||||
- CORS middleware
|
- CORS middleware
|
||||||
- Optional POST data validation middleware with Pydantic
|
- Optional POST data validation middleware with Pydantic
|
||||||
- Session middleware with built-in session store
|
- Session middleware with built-in session store
|
||||||
- Database support (using Peewee, but you can use whatever you want as long as there's a Peewee driver for it)
|
- Database support (using SQLAlchemy, but you can use whatever you want as long as there's a SQLAlchemy driver for it)
|
||||||
- Tests (currently roughly 89% coverage)
|
- Tests (currently roughly 89% coverage)
|
||||||
|
|
|
@ -62,11 +62,12 @@ Spiderweb provides five types of responses out of the box:
|
||||||
|
|
||||||
### Database Agnosticism (Mostly)
|
### Database Agnosticism (Mostly)
|
||||||
|
|
||||||
One of the largest selling points of Django is the Django Object Relational Mapper (ORM); while there's nothing that compares to it in functionality, there are many other ORMs and database management solutions for developers to choose from.
|
Spiderweb persists its internal data (like sessions) using Advanced Alchemy built on SQLAlchemy. Your application can use this same setup out of the box, or bring any ORM you prefer.
|
||||||
|
|
||||||
In order to use a database internally (and since this is not about writing an ORM too), Spiderweb depends on [peewee, a small ORM](https://github.com/coleifer/peewee). Applications using Spiderweb are more than welcome to use peewee models with first-class support or use whatever they're familiar with. Peewee supports PostgreSQL, MySQL, Sqlite, and CockroachDB; if you use one of these, Spiderweb can create the tables it needs in your database and stay out of the way. By default, Spiderweb creates a sqlite database in the application directory for its own use.
|
- By default, Spiderweb creates a SQLite database file `spiderweb.db` next to your app.
|
||||||
|
- You can pass the `db` argument to `SpiderwebRouter` as a filesystem path (for SQLite), a SQLAlchemy database URL string, or a SQLAlchemy Engine instance.
|
||||||
|
|
||||||
> [Read more about the using a database in Spiderweb](db.md)
|
> [Read more about databases and migrations](db.md)
|
||||||
|
|
||||||
### Easy to configure
|
### Easy to configure
|
||||||
|
|
||||||
|
@ -129,4 +130,4 @@ Here's a non-exhaustive list of things this can do:
|
||||||
- Database support (using Peewee, but you can use whatever you want as long as there's a Peewee driver for it)
|
- Database support (using Peewee, but you can use whatever you want as long as there's a Peewee driver for it)
|
||||||
- Tests (currently a little over 80% coverage)
|
- Tests (currently a little over 80% coverage)
|
||||||
|
|
||||||
[^1]: I mostly succeeded. The way that I'm approaching this is that I did my level best, then looked at (and copied) existing solutions where necessary. At the time of this writing, I did all of it solo except for the CORS middleware. [Read more about it here.](middleware/cors.md)
|
[^1]: I mostly succeeded. The way that I'm approaching this is that I did my level best, then looked at (and copied) existing solutions where necessary. At the time of this writing, I did all of it solo except for the CORS middleware. [Read more about it here.](middleware/cors.md)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||

|

|
||||||
|
|
||||||
> the web framework just big enough for a spider
|
> the web framework just big enough for a spider
|
||||||
|
|
||||||
|
|
BIN
docs/_media/spiderweb_logo_cropped.png
Normal file
BIN
docs/_media/spiderweb_logo_cropped.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 173 KiB |
|
@ -9,5 +9,6 @@
|
||||||
- [csrf](middleware/csrf.md)
|
- [csrf](middleware/csrf.md)
|
||||||
- [cors](middleware/cors.md)
|
- [cors](middleware/cors.md)
|
||||||
- [pydantic](middleware/pydantic.md)
|
- [pydantic](middleware/pydantic.md)
|
||||||
|
- [gzip](middleware/gzip.md)
|
||||||
- [writing your own](middleware/custom_middleware.md)
|
- [writing your own](middleware/custom_middleware.md)
|
||||||
- [databases](db.md)
|
- [databases](db.md)
|
||||||
|
|
145
docs/db.md
145
docs/db.md
|
@ -1,19 +1,39 @@
|
||||||
# databases
|
# databases
|
||||||
|
|
||||||
It's hard to find a server-side app without a database these days, and for good reason: there are a lot of things to keep track of. Spiderweb does its best to remain database-agnostic, though it does utilize `peewee` internally to handle its own data (such as session data). This means that you have three options for how to handle databases in your app.
|
Spiderweb is intentionally ORM-agnostic. Internally, it now uses Advanced Alchemy (built on SQLAlchemy) to persist first‑party data like sessions. You can choose one of the following approaches for your application data:
|
||||||
|
|
||||||
## Option 1: Using Peewee
|
- Option 1: Use the built‑in Advanced Alchemy/SQLAlchemy setup
|
||||||
|
- Option 2: Bring your own ORM and manage its lifecycle
|
||||||
|
- Option 3: Use separate databases for Spiderweb internals and your app data
|
||||||
|
|
||||||
If you'd just like to use the same system that's already in place, you can import `SpiderwebModel` and get to work writing your own models for Peewee. See below for notes on writing your own database models and fitting them into the server and for changing the driver to a different type of database.
|
## Option 1: Use the built‑in Advanced Alchemy/SQLAlchemy setup
|
||||||
|
|
||||||
|
By default, Spiderweb will create and use a SQLite database file named `spiderweb.db` in your application directory. You can change this by passing the `db` argument to `SpiderwebRouter` as any of the following:
|
||||||
|
|
||||||
## Option 2: Using your own database ORM
|
- A SQLAlchemy Engine instance
|
||||||
|
- A database URL string (e.g., `sqlite:///path/to.db`, `postgresql+psycopg://user:pass@host/db`)
|
||||||
|
- A filesystem path string for SQLite (e.g., `my_db.sqlite`)
|
||||||
|
|
||||||
You may not want to use Peewee, and that's totally fine; in that case, you will want to tell Spiderweb where the database is so that it can create the tables that it needs. To do this, you'll need to be be using a database type that Peewee supports; at this time, the options are SQLite, MySQL, MariaDB, and Postgres.
|
Examples:
|
||||||
|
|
||||||
You'll want to instantiate your own ORM in the way that works best for you and let Spiderweb know where to find the database. See "Changing the Peewee Database Target" below for information on how to adjust where Spiderweb places data.
|
```python
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from spiderweb import SpiderwebRouter
|
||||||
|
|
||||||
Instantiating your own ORM depends on whether your ORM can maintain an application-wide connection or if it needs a new connection on a per-request basis. For example, SQLAlchemy prefers that you use an `engine` to access the database. Since it's not clear at any given point which view will be receiving a request, this might be a good reason for some custom middleware to add an `engine` attribute onto the request that can be retrieved later:
|
# Use a SQLite file by passing a path
|
||||||
|
app = SpiderwebRouter(db="my_db.sqlite")
|
||||||
|
|
||||||
|
# Or pass a SQLAlchemy engine
|
||||||
|
engine = create_engine("postgresql+psycopg://user:pass@localhost/myapp")
|
||||||
|
app = SpiderwebRouter(db=engine)
|
||||||
|
|
||||||
|
# Or pass a full URL string
|
||||||
|
app = SpiderwebRouter(db="sqlite:///./local.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option 2: Bring your own ORM
|
||||||
|
|
||||||
|
If you are using another ORM or data layer, create and manage it as you normally would. If you need per‑request access to a connection or session, you can attach it via custom middleware:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from spiderweb.middleware import SpiderwebMiddleware
|
from spiderweb.middleware import SpiderwebMiddleware
|
||||||
|
@ -21,105 +41,60 @@ from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyMiddleware(SpiderwebMiddleware):
|
class SQLAlchemyMiddleware(SpiderwebMiddleware):
|
||||||
# there's only one of these, so we can just make it a top-level attr
|
|
||||||
engine = None
|
engine = None
|
||||||
|
|
||||||
def process_request(self, request) -> None:
|
def process_request(self, request) -> None:
|
||||||
# provide handles for the default `spiderweb.db` sqlite3 db
|
|
||||||
if not self.engine:
|
if not self.engine:
|
||||||
self.engine = create_engine("sqlite:///spiderweb.db")
|
self.engine = create_engine("sqlite:///spiderweb.db")
|
||||||
request.engine = self.engine
|
request.engine = self.engine
|
||||||
```
|
```
|
||||||
Now, any view that receives the incoming request object will be able to access `request.engine` and interact with the database as needed.
|
|
||||||
|
Now any view that receives the incoming request object can access `request.engine` and interact with the database as needed.
|
||||||
|
|
||||||
> See [Writing Your Own Middleware](middleware/custom_middleware.md) for more information.
|
> See [Writing Your Own Middleware](middleware/custom_middleware.md) for more information.
|
||||||
|
|
||||||
## Option 3: Using two databases
|
## Option 3: Use two databases
|
||||||
|
|
||||||
While this isn't the most delightful of options, admittedly, if your application needs to use a database that isn't something Peewee natively supports, you will want to set aside a database connection specifically for Spiderweb so that internal functions will continue to work as expected while your app uses the database you need for business logic.
|
If your application requires a database not supported by SQLAlchemy or you prefer to keep concerns separated, you can run two databases: one for Spiderweb internals (sessions, etc.) and one for your application logic.
|
||||||
|
|
||||||
## Changing the Peewee Database Target
|
## Migrations
|
||||||
|
|
||||||
By default, Spiderweb will create and use a SQLite db in the application directory named `spiderweb.db`. You can change this by selecting the right driver from Peewee and passing it to Spiderweb during the server instantiation, like this:
|
Advanced Alchemy works seamlessly with Alembic (SQLAlchemy's migration tool). To manage schema changes:
|
||||||
|
|
||||||
```python
|
1. Install Alembic:
|
||||||
from spiderweb import SpiderwebRouter
|
|
||||||
from peewee import SqliteDatabase
|
|
||||||
|
|
||||||
app = SpiderwebRouter(
|
```bash
|
||||||
db=SqliteDatabase("my_db.sqlite")
|
pip install alembic
|
||||||
)
|
```
|
||||||
```
|
|
||||||
|
|
||||||
Peewee supports the following databases at this time:
|
2. Initialize a migration repository:
|
||||||
|
|
||||||
- SQLite
|
```bash
|
||||||
- MySQL
|
alembic init migrations
|
||||||
- MariaDB
|
```
|
||||||
- Postgres
|
|
||||||
|
|
||||||
Connecting Spiderweb to Postgres would look like this:
|
3. Configure Alembic to use Spiderweb's metadata. In `migrations/env.py`, set:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from spiderweb import SpiderwebRouter
|
from spiderweb.db import Base
|
||||||
from peewee import PostgresqlDatabase
|
target_metadata = Base.metadata
|
||||||
|
```
|
||||||
|
|
||||||
app = SpiderwebRouter(
|
Also set the database URL either in `alembic.ini` (`sqlalchemy.url = ...`) or dynamically in `env.py` (read from environment variables or config).
|
||||||
db = PostgresqlDatabase(
|
|
||||||
'my_app',
|
|
||||||
user='postgres',
|
|
||||||
password='secret',
|
|
||||||
host='10.1.0.9',
|
|
||||||
port=5432
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Writing Peewee Models
|
4. Generate migrations from model changes:
|
||||||
|
|
||||||
```python
|
```bash
|
||||||
from spiderweb.db import SpiderwebModel
|
alembic revision --autogenerate -m "add my table"
|
||||||
```
|
```
|
||||||
|
|
||||||
If you'd like to use Peewee, then you can use the model code written for Spiderweb. There are two special powers this grants you: migration checking and automatic database assignments.
|
5. Apply migrations:
|
||||||
|
|
||||||
### Automatic Database Assignments
|
```bash
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
One of the odder quirks of Peewee is that you must specify what database object a model is attached to. From [the docs](https://docs.peewee-orm.com/en/latest/peewee/quickstart.html#model-definition):
|
Notes:
|
||||||
|
- If you define your own SQLAlchemy models, make sure they inherit from `spiderweb.db.Base` (or include their metadata in `target_metadata`) so Alembic can discover them.
|
||||||
```python
|
- For multi-database setups, you can configure multiple Alembic contexts or run Alembic separately per database.
|
||||||
from peewee import *
|
- Advanced Alchemy provides additional helpers on top of SQLAlchemy; you can use them freely alongside the guidance above.
|
||||||
|
|
||||||
db = SqliteDatabase('people.db')
|
|
||||||
|
|
||||||
class Person(Model):
|
|
||||||
name = CharField()
|
|
||||||
birthday = DateField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
database = db # This model uses the "people.db" database.
|
|
||||||
```
|
|
||||||
|
|
||||||
Spiderweb handles the database assignment for you so that your model is added to the same database that is already in use, regardless of driver:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from spiderweb.db import SpiderwebModel
|
|
||||||
from peewee import *
|
|
||||||
|
|
||||||
class Person(SpiderwebModel):
|
|
||||||
name = CharField()
|
|
||||||
birthday = DateField()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Migration Checking
|
|
||||||
|
|
||||||
Spiderweb also watches your model and raises an error if the state of the database and your model schema differ. This check attempts to be as thorough as possible, but may not be appropriate for you; if that's the case, then add the magic variable `skip_migration_check` to the `Meta` class for your model. For example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Person(SpiderwebModel):
|
|
||||||
name = CharField()
|
|
||||||
birthday = DateField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
skip_migration_check = True
|
|
||||||
```
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ class TestMiddleware(SpiderwebMiddleware):
|
||||||
|
|
||||||
Middleware is run twice: once for the incoming request and once for the outgoing response. You only need to include whichever function is required for the functionality you need.
|
Middleware is run twice: once for the incoming request and once for the outgoing response. You only need to include whichever function is required for the functionality you need.
|
||||||
|
|
||||||
## process_request(self, request):
|
## process_request(self, request: Request) -> Optional[HttpResponse]:
|
||||||
|
|
||||||
`process_request` is called before the view is reached in the execution order. You will receive the assembled Request object, and any middleware declared above this one will have already run. Because the request is the single instantiation of a class, you can modify it in-place without returning anything and your changes will stick.
|
`process_request` is called before the view is reached in the execution order. You will receive the assembled Request object, and any middleware declared above this one will have already run. Because the request is the single instantiation of a class, you can modify it in-place without returning anything and your changes will stick.
|
||||||
|
|
||||||
|
@ -45,15 +45,75 @@ class JohnMiddleware(SpiderwebMiddleware):
|
||||||
|
|
||||||
In this case, if the user John tries to access any route that starts with "/admin", he'll immediately get denied and the view will never be called. If the request does not have a user attached to it (or the user is not John), then the middleware will return None and Spiderweb will continue processing.
|
In this case, if the user John tries to access any route that starts with "/admin", he'll immediately get denied and the view will never be called. If the request does not have a user attached to it (or the user is not John), then the middleware will return None and Spiderweb will continue processing.
|
||||||
|
|
||||||
## process_response(self, request, response):
|
## process_response(self, request: Request, response: HttpResponse) -> None:
|
||||||
|
|
||||||
This function is called after the view has run and returned a response. You will receive the request object and the response object; like with the request object, the response is also a single instantiation of a class, so any changes you make will stick automatically.
|
This function is called after the view has run and returned a response. You will receive the request object and the response object; like with the request object, the response is also a single instantiation of a class, so any changes you make will stick automatically.
|
||||||
|
|
||||||
Unlike `process_request`, returning a value here doesn't change anything. We're already processing a request, and there are opportunities to turn away requests / change the response at both the `process_request` layer and the view layer, so Spiderweb assumes that whatever it is working on here is what you mean to return to the user. The response object that you receive in the middleware is still prerendered, so any changes you make to it will take effect after it finishes the middleware and renders the response.
|
Unlike `process_request`, returning a value here doesn't change anything. We're already processing a request, and there are opportunities to turn away requests / change the response at both the `process_request` layer and the view layer, so Spiderweb assumes that whatever it is working on here is what you mean to return to the user. The response object that you receive in the middleware is still prerendered, so any changes you make to it will take effect after it finishes the middleware and renders the response.
|
||||||
|
|
||||||
## on_error(self, request, triggered_exception):
|
## on_error(self, request: Request, triggered_exception: Exception) -> Optional[HttpResponse]:
|
||||||
|
|
||||||
This is a helper function that is available for you to override; it's not often used by middleware, but there are some ([like the pydantic middleware](pydantic.md)) that call `on_error` when there is a validation failure.
|
This is a helper function that is available for you to override; it's not often used by middleware, but there are some ([like the pydantic middleware](middleware/pydantic.md)) that call `on_error` when there is a validation failure.
|
||||||
|
|
||||||
|
## post_process(self, request: Request, response: HttpResponse, rendered_response: str) -> str | bytes:
|
||||||
|
|
||||||
|
> New in 1.3.0!
|
||||||
|
|
||||||
|
After `process_request` and `process_response` run, the response is rendered out into the raw text that is going to be sent to the client. Right before that happens, `post_process` is called on each middleware in the same order as `process_response` (so the closer something is to the beginning of the middleware list, the more important it is).
|
||||||
|
|
||||||
|
There are three things passed to `post_process`:
|
||||||
|
|
||||||
|
- `request`: the request object. It's provided here purely for reference purposes; while you can technically change it here, it won't have any effect on the response.
|
||||||
|
- `response`: the response object. The full HTML of the response has already been rendered, but the headers can still be modified here. This object can be modified in place, like in `process_response`.
|
||||||
|
- `rendered_response`: the full HTML of the response as a string or bytes. This is the final output that will be sent to the client. Every instance of `post_process` must return the full HTML of the response, so if you want to make changes, you'll need to return the modified string. A string is _strongly_ preferred, but bytes are also acceptable; keep in mind that you'll be making things harder for any `post_process` middleware that comes after you.
|
||||||
|
|
||||||
|
Note that this function *must* return the full HTML of the response (provided at the start as `rendered_response`. Each invocation of `post_process` overwrites the entire output of the response, so make sure to return everything that you want to send. For example, here's a middleware that ~~breaks~~ adjusts the capitalization of the response and also demonstrates passing variables into the middleware and modifies the headers with the type of transformation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import random
|
||||||
|
|
||||||
|
from spiderweb.request import Request
|
||||||
|
from spiderweb.response import HttpResponse
|
||||||
|
from spiderweb.middleware import SpiderwebMiddleware
|
||||||
|
from spiderweb.exceptions import ConfigError
|
||||||
|
|
||||||
|
|
||||||
|
class CaseTransformMiddleware(SpiderwebMiddleware):
|
||||||
|
# this breaks everything, but it's hilarious so it's worth it. Blame Sam.
|
||||||
|
def post_process(self, request: Request, response: HttpResponse, rendered_response: str) -> str:
|
||||||
|
valid_options = ["spongebob", "random"]
|
||||||
|
# grab the value from the extra data passed into the server object
|
||||||
|
# during instantiation
|
||||||
|
method = self.server.extra_data.get("case_transform_middleware_type", "spongebob")
|
||||||
|
if method not in valid_options:
|
||||||
|
raise ConfigError(
|
||||||
|
f"Invalid method '{method}' for CaseTransformMiddleware."
|
||||||
|
f" Valid options are {', '.join(valid_options)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if method == "spongebob":
|
||||||
|
response.headers["X-Case-Transform"] = "spongebob"
|
||||||
|
return "".join(
|
||||||
|
char.upper()
|
||||||
|
if i % 2 == 0
|
||||||
|
else char.lower() for i, char in enumerate(rendered_response)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response.headers["X-Case-Transform"] = "random"
|
||||||
|
return "".join(
|
||||||
|
char.upper()
|
||||||
|
if random.random() > 0.5
|
||||||
|
else char for char in rendered_response
|
||||||
|
)
|
||||||
|
|
||||||
|
# usage:
|
||||||
|
from spiderweb import SpiderwebRouter
|
||||||
|
|
||||||
|
app = SpiderwebRouter(
|
||||||
|
middleware=["CaseTransformMiddleware"],
|
||||||
|
case_transform_middleware_type="random",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
## checks
|
## checks
|
||||||
|
|
||||||
|
@ -109,4 +169,4 @@ List as many checks as you need there, and the server will run all of them durin
|
||||||
from spiderweb.exceptions import UnusedMiddleware
|
from spiderweb.exceptions import UnusedMiddleware
|
||||||
```
|
```
|
||||||
|
|
||||||
If you don't want your middleware to run for some reason, either `process_request` or `process_response` can raise the UnusedMiddleware exception. If this happens, Spiderweb will kick your middleware out of the processing order for the rest of the life of the server. Note that this applies to the middleware as a whole, so both functions will not be run if an UnusedMiddleware is raised. This is a great way to mark debug middleware that shouldn't run or create time-delay middleware that runs until a certain condition is met!
|
If you don't want your middleware to run for some reason, `process_request`, `process_response` and `post_process` can all raise the UnusedMiddleware exception. If this happens, Spiderweb will kick your middleware out of the processing order for the rest of the life of the server. Note that this applies to the middleware as a whole, so all functions in the middleware will not be run if an UnusedMiddleware is raised. This is a great way to mark debug middleware that shouldn't run or create time-delay middleware that runs until a certain condition is met!
|
||||||
|
|
51
docs/middleware/gzip.md
Normal file
51
docs/middleware/gzip.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# gzip compression middleware
|
||||||
|
|
||||||
|
> New in 1.4.0!
|
||||||
|
|
||||||
|
```python
|
||||||
|
from spiderweb import SpiderwebRouter
|
||||||
|
|
||||||
|
app = SpiderwebRouter(
|
||||||
|
middleware=["spiderweb.middleware.gzip.GzipMiddleware"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If your app is serving large responses, you may want to compress them. We don't (currently) have built-in support for Brotli, deflate, zstd, or other compression methods, but we do support gzip. (Want to add support for other methods? We'd love to see a PR!)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `GzipMiddleware` must be the as close to the 'top' of the middleware stack as possible. If you have multiple middleware, make sure that `gzip` is the first one in the list. For example:
|
||||||
|
> ```python
|
||||||
|
> app = SpiderwebRouter(
|
||||||
|
> middleware=[
|
||||||
|
> "spiderweb.middleware.gzip.GzipMiddleware",
|
||||||
|
> "spiderweb.middleware.cors.CorsMiddleware",
|
||||||
|
> "spiderweb.middleware.csrf.CSRFMiddleware",
|
||||||
|
> ]
|
||||||
|
> )
|
||||||
|
> ```
|
||||||
|
|
||||||
|
The implementation in Spiderweb is simple: it compresses the response body if the client indicates that it is supported. If the client doesn't support gzip, the response is sent uncompressed. Compression happens at the end of the response cycle, so it won't interfere with other middleware.
|
||||||
|
|
||||||
|
Error responses and responses with status codes that indicate that the response body should not be sent (like 204, 304, etc.) will not be compressed. Responses with a `Content-Encoding` header already set (e.g. if you're serving pre-compressed files) will be handled the same way.
|
||||||
|
|
||||||
|
The available configuration options are:
|
||||||
|
|
||||||
|
## gzip_minimum_response_length
|
||||||
|
|
||||||
|
The minimum size in bytes of a response before it will be compressed. Defaults to `500`. Responses smaller than this will not be compressed.
|
||||||
|
|
||||||
|
```python
|
||||||
|
app = SpiderwebRouter(
|
||||||
|
gzip_minimum_response_length=1000
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## gzip_compression_level
|
||||||
|
|
||||||
|
The level of compression to use. Defaults to `6`. This is a number between 0 and 9, where 0 is no compression and 9 is maximum compression. Higher levels will result in smaller files, but will take longer to compress and decompress. Level 6 is a good balance between file size and speed.
|
||||||
|
|
||||||
|
```python
|
||||||
|
app = SpiderwebRouter(
|
||||||
|
gzip_compression_level=9
|
||||||
|
)
|
||||||
|
```
|
|
@ -9,6 +9,9 @@ app = SpiderwebRouter(
|
||||||
```
|
```
|
||||||
When working with form data, you may not want to always have to perform your own validation on the incoming data. Spiderweb gives you a way out of the box to perform this validation using Pydantic.
|
When working with form data, you may not want to always have to perform your own validation on the incoming data. Spiderweb gives you a way out of the box to perform this validation using Pydantic.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Pydantic is not installed by default. Install it with `pip install pydantic` or `pip install spiderweb[pydantic]`.
|
||||||
|
|
||||||
Let's assume that we have a form view that looks like this:
|
Let's assume that we have a form view that looks like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -54,4 +57,4 @@ The Pydantic middleware will automatically detect that the model that you want t
|
||||||
|
|
||||||
If the validation fails, the middleware will call `on_error`, which by default will return a 400 with a list of the broken fields. You may not want this behavior, so the easiest way to address it is to subclass PydanticMiddleware with your own version and override `on_error` to do whatever you'd like.
|
If the validation fails, the middleware will call `on_error`, which by default will return a 400 with a list of the broken fields. You may not want this behavior, so the easiest way to address it is to subclass PydanticMiddleware with your own version and override `on_error` to do whatever you'd like.
|
||||||
|
|
||||||
If validation succeeds, the data from the validator will appear on the request object under `request.validated_data` — to access it, just call `.dict()` on the validated data.
|
If validation succeeds, the data from the validator will appear on the request object under `request.validated_data` — to access it, just call `.dict()` on the validated data.
|
||||||
|
|
|
@ -4,10 +4,10 @@ Start by installing the package with your favorite package manager:
|
||||||
|
|
||||||
<!-- tabs:start -->
|
<!-- tabs:start -->
|
||||||
|
|
||||||
<!-- tab:poetry -->
|
<!-- tab:uv -->
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
poetry add spiderweb-framework
|
uv add spiderweb-framework
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- tab:pip -->
|
<!-- tab:pip -->
|
||||||
|
|
|
@ -15,6 +15,7 @@ from spiderweb.response import (
|
||||||
app = SpiderwebRouter(
|
app = SpiderwebRouter(
|
||||||
templates_dirs=["templates"],
|
templates_dirs=["templates"],
|
||||||
middleware=[
|
middleware=[
|
||||||
|
"spiderweb.middleware.gzip.GzipMiddleware",
|
||||||
"spiderweb.middleware.cors.CorsMiddleware",
|
"spiderweb.middleware.cors.CorsMiddleware",
|
||||||
"spiderweb.middleware.sessions.SessionMiddleware",
|
"spiderweb.middleware.sessions.SessionMiddleware",
|
||||||
"spiderweb.middleware.csrf.CSRFMiddleware",
|
"spiderweb.middleware.csrf.CSRFMiddleware",
|
||||||
|
@ -22,12 +23,14 @@ app = SpiderwebRouter(
|
||||||
"example_middleware.RedirectMiddleware",
|
"example_middleware.RedirectMiddleware",
|
||||||
"spiderweb.middleware.pydantic.PydanticMiddleware",
|
"spiderweb.middleware.pydantic.PydanticMiddleware",
|
||||||
"example_middleware.ExplodingMiddleware",
|
"example_middleware.ExplodingMiddleware",
|
||||||
|
# "example_middleware.CaseTransformMiddleware",
|
||||||
],
|
],
|
||||||
staticfiles_dirs=["static_files"],
|
staticfiles_dirs=["static_files"],
|
||||||
append_slash=False, # default
|
append_slash=False, # default
|
||||||
cors_allow_all_origins=True,
|
cors_allow_all_origins=True,
|
||||||
static_url="static_stuff",
|
static_url="static_stuff",
|
||||||
debug=True,
|
debug=True,
|
||||||
|
case_transform_middleware_type="spongebob",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -48,12 +48,14 @@ def form(request):
|
||||||
app = SpiderwebRouter(
|
app = SpiderwebRouter(
|
||||||
templates_dirs=["templates"],
|
templates_dirs=["templates"],
|
||||||
middleware=[
|
middleware=[
|
||||||
|
"spiderweb.middleware.sessions.SessionMiddleware",
|
||||||
"spiderweb.middleware.csrf.CSRFMiddleware",
|
"spiderweb.middleware.csrf.CSRFMiddleware",
|
||||||
"example_middleware.TestMiddleware",
|
"example_middleware.TestMiddleware",
|
||||||
"example_middleware.RedirectMiddleware",
|
"example_middleware.RedirectMiddleware",
|
||||||
"example_middleware.ExplodingMiddleware",
|
"example_middleware.ExplodingMiddleware",
|
||||||
],
|
],
|
||||||
staticfiles_dirs=["static_files"],
|
staticfiles_dirs=["static_files"],
|
||||||
|
debug=True,
|
||||||
routes=[
|
routes=[
|
||||||
("/", index),
|
("/", index),
|
||||||
("/redirect", redirect),
|
("/redirect", redirect),
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import random
|
||||||
|
|
||||||
|
from spiderweb import ConfigError
|
||||||
from spiderweb.exceptions import UnusedMiddleware
|
from spiderweb.exceptions import UnusedMiddleware
|
||||||
from spiderweb.middleware import SpiderwebMiddleware
|
from spiderweb.middleware import SpiderwebMiddleware
|
||||||
from spiderweb.request import Request
|
from spiderweb.request import Request
|
||||||
|
@ -24,3 +27,35 @@ class RedirectMiddleware(SpiderwebMiddleware):
|
||||||
class ExplodingMiddleware(SpiderwebMiddleware):
|
class ExplodingMiddleware(SpiderwebMiddleware):
|
||||||
def process_request(self, request: Request) -> HttpResponse | None:
|
def process_request(self, request: Request) -> HttpResponse | None:
|
||||||
raise UnusedMiddleware("Unfinished!")
|
raise UnusedMiddleware("Unfinished!")
|
||||||
|
|
||||||
|
|
||||||
|
class CaseTransformMiddleware(SpiderwebMiddleware):
|
||||||
|
# this breaks everything, but it's hilarious so it's worth it.
|
||||||
|
# Blame Sam.
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
valid_options = ["spongebob", "random"]
|
||||||
|
# grab the value from the extra data passed into the server object
|
||||||
|
# during instantiation
|
||||||
|
method = self.server.extra_data.get(
|
||||||
|
"case_transform_middleware_type", "spongebob"
|
||||||
|
)
|
||||||
|
if method not in valid_options:
|
||||||
|
raise ConfigError(
|
||||||
|
f"Invalid method '{method}' for CaseTransformMiddleware."
|
||||||
|
f" Valid options are {', '.join(valid_options)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if method == "spongebob":
|
||||||
|
response.headers["X-Case-Transform"] = "spongebob"
|
||||||
|
return "".join(
|
||||||
|
char.upper() if i % 2 == 0 else char.lower()
|
||||||
|
for i, char in enumerate(rendered_response)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response.headers["X-Case-Transform"] = "random"
|
||||||
|
return "".join(
|
||||||
|
char.upper() if random.random() > 0.5 else char
|
||||||
|
for char in rendered_response
|
||||||
|
)
|
||||||
|
|
685
poetry.lock
generated
685
poetry.lock
generated
|
@ -1,11 +1,61 @@
|
||||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "advanced-alchemy"
|
||||||
|
version = "1.6.3"
|
||||||
|
description = "Ready-to-go SQLAlchemy concoctions."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "advanced_alchemy-1.6.3-py3-none-any.whl", hash = "sha256:d905f51affb427a13787f4b562d283ee891fc3c3865b5d7869f2f483533f1546"},
|
||||||
|
{file = "advanced_alchemy-1.6.3.tar.gz", hash = "sha256:b0ed313c0e1b7ac3c1a9caf8349d1742099b1c1d5f73e8926826da942aa1bf6c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
alembic = ">=1.12.0"
|
||||||
|
greenlet = "*"
|
||||||
|
sqlalchemy = ">=2.0.20"
|
||||||
|
typing-extensions = ">=4.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
argon2 = ["argon2-cffi"]
|
||||||
|
cli = ["rich-click"]
|
||||||
|
fsspec = ["fsspec"]
|
||||||
|
nanoid = ["fastnanoid (>=0.4.1)"]
|
||||||
|
obstore = ["obstore"]
|
||||||
|
passlib = ["passlib[argon2]"]
|
||||||
|
pwdlib = ["pwdlib[argon2]"]
|
||||||
|
uuid = ["uuid-utils (>=0.6.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alembic"
|
||||||
|
version = "1.17.0"
|
||||||
|
description = "A database migration tool for SQLAlchemy."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "alembic-1.17.0-py3-none-any.whl", hash = "sha256:80523bc437d41b35c5db7e525ad9d908f79de65c27d6a5a5eab6df348a352d99"},
|
||||||
|
{file = "alembic-1.17.0.tar.gz", hash = "sha256:4652a0b3e19616b57d652b82bfa5e38bf5dbea0813eed971612671cb9e90c0fe"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Mako = "*"
|
||||||
|
SQLAlchemy = ">=1.4.0"
|
||||||
|
typing-extensions = ">=4.12"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tz = ["tzdata"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
description = "Reusable constraint types to use with typing.Annotated"
|
description = "Reusable constraint types to use with typing.Annotated"
|
||||||
optional = false
|
optional = true
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "extra == \"pydantic\""
|
||||||
files = [
|
files = [
|
||||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||||
|
@ -17,18 +67,19 @@ version = "24.2.0"
|
||||||
description = "Classes Without Boilerplate"
|
description = "Classes Without Boilerplate"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
|
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
|
||||||
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
|
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
|
||||||
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
|
||||||
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
|
||||||
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||||
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
|
||||||
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
|
@ -36,6 +87,7 @@ version = "24.8.0"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
|
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
|
||||||
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
|
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
|
||||||
|
@ -70,95 +122,17 @@ platformdirs = ">=2"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""]
|
||||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cffi"
|
|
||||||
version = "1.17.0"
|
|
||||||
description = "Foreign Function Interface for Python calling C code."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"},
|
|
||||||
{file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"},
|
|
||||||
{file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
|
|
||||||
{file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"},
|
|
||||||
{file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"},
|
|
||||||
{file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"},
|
|
||||||
{file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"},
|
|
||||||
{file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pycparser = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
|
@ -173,6 +147,8 @@ version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
groups = ["dev"]
|
||||||
|
markers = "sys_platform == \"win32\" or platform_system == \"Windows\""
|
||||||
files = [
|
files = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
@ -184,6 +160,7 @@ version = "7.6.1"
|
||||||
description = "Code coverage measurement for Python"
|
description = "Code coverage measurement for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
|
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
|
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
|
||||||
|
@ -260,91 +237,75 @@ files = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
toml = ["tomli"]
|
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "greenlet"
|
||||||
version = "43.0.0"
|
version = "3.2.4"
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
description = "Lightweight in-process concurrent programming"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
|
{file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
|
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
|
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
|
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
|
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
|
{file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
|
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
|
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"},
|
||||||
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
|
{file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
|
{file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
|
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
|
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
|
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
|
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
|
{file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
|
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
|
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"},
|
||||||
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
|
{file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"},
|
||||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
|
{file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"},
|
||||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
|
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"},
|
||||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
|
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968"},
|
||||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
|
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9"},
|
||||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
|
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6"},
|
||||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
|
{file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"},
|
||||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
|
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"},
|
||||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
|
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"},
|
||||||
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
|
{file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"},
|
||||||
]
|
{file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"},
|
||||||
|
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"},
|
||||||
[package.dependencies]
|
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc"},
|
||||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a"},
|
||||||
|
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504"},
|
||||||
[package.extras]
|
{file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"},
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"},
|
||||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"},
|
||||||
nox = ["nox"]
|
{file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"},
|
||||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
{file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"},
|
||||||
sdist = ["build"]
|
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"},
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5"},
|
||||||
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"},
|
||||||
test-randomorder = ["pytest-randomly"]
|
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"},
|
||||||
|
{file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"},
|
||||||
[[package]]
|
{file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"},
|
||||||
name = "dnspython"
|
{file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"},
|
||||||
version = "2.6.1"
|
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"},
|
||||||
description = "DNS toolkit"
|
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58"},
|
||||||
optional = false
|
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4"},
|
||||||
python-versions = ">=3.8"
|
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433"},
|
||||||
files = [
|
{file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"},
|
||||||
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
|
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"},
|
||||||
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
|
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"},
|
||||||
|
{file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"},
|
||||||
|
{file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"},
|
||||||
|
{file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
|
docs = ["Sphinx", "furo"]
|
||||||
dnssec = ["cryptography (>=41)"]
|
test = ["objgraph", "psutil", "setuptools"]
|
||||||
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
|
|
||||||
doq = ["aioquic (>=0.9.25)"]
|
|
||||||
idna = ["idna (>=3.6)"]
|
|
||||||
trio = ["trio (>=0.23)"]
|
|
||||||
wmi = ["wmi (>=1.5.1)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "email-validator"
|
|
||||||
version = "2.2.0"
|
|
||||||
description = "A robust email address syntax and deliverability validation library."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
|
|
||||||
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
dnspython = ">=2.0.0"
|
|
||||||
idna = ">=2.0.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
|
@ -352,6 +313,7 @@ version = "23.0.0"
|
||||||
description = "WSGI HTTP Server for UNIX"
|
description = "WSGI HTTP Server for UNIX"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
|
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
|
||||||
{file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
|
{file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
|
||||||
|
@ -369,13 +331,14 @@ tornado = ["tornado (>=0.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hypothesis"
|
name = "hypothesis"
|
||||||
version = "6.111.2"
|
version = "6.112.1"
|
||||||
description = "A library for property-based testing"
|
description = "A library for property-based testing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "hypothesis-6.111.2-py3-none-any.whl", hash = "sha256:055e8228958e22178d6077e455fd86a72044d02dac130dbf9c8b31e161b9809c"},
|
{file = "hypothesis-6.112.1-py3-none-any.whl", hash = "sha256:93631b1498b20d2c205ed304cbd41d50e9c069d78a9c773c1324ca094c5e30ce"},
|
||||||
{file = "hypothesis-6.111.2.tar.gz", hash = "sha256:0496ad28c7240ee9ba89fcc7fb1dc74e89f3e40fbcbbb5f73c0091558dec8e6e"},
|
{file = "hypothesis-6.112.1.tar.gz", hash = "sha256:b070d7a1bb9bd84706c31885c9aeddc138e2b36a9c112a91984f49501c567856"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -383,7 +346,7 @@ attrs = ">=22.2.0"
|
||||||
sortedcontainers = ">=2.1.0,<3.0.0"
|
sortedcontainers = ">=2.1.0,<3.0.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.70)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.13)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"]
|
all = ["backports.zoneinfo (>=0.2.1) ; python_version < \"3.9\"", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.70)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.13)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""]
|
||||||
cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
|
cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
|
||||||
codemods = ["libcst (>=0.3.16)"]
|
codemods = ["libcst (>=0.3.16)"]
|
||||||
crosshair = ["crosshair-tool (>=0.0.70)", "hypothesis-crosshair (>=0.0.13)"]
|
crosshair = ["crosshair-tool (>=0.0.70)", "hypothesis-crosshair (>=0.0.13)"]
|
||||||
|
@ -397,18 +360,7 @@ pandas = ["pandas (>=1.1)"]
|
||||||
pytest = ["pytest (>=4.6)"]
|
pytest = ["pytest (>=4.6)"]
|
||||||
pytz = ["pytz (>=2014.1)"]
|
pytz = ["pytz (>=2014.1)"]
|
||||||
redis = ["redis (>=3.0.0)"]
|
redis = ["redis (>=3.0.0)"]
|
||||||
zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"]
|
zoneinfo = ["backports.zoneinfo (>=0.2.1) ; python_version < \"3.9\"", "tzdata (>=2024.1) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.7"
|
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
files = [
|
|
||||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
|
||||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
|
@ -416,27 +368,31 @@ version = "2.0.0"
|
||||||
description = "brain-dead simple config-ini parsing"
|
description = "brain-dead simple config-ini parsing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "mako"
|
||||||
version = "3.1.4"
|
version = "1.3.10"
|
||||||
description = "A very fast and expressive template engine."
|
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
{file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"},
|
||||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
{file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
MarkupSafe = ">=2.0"
|
MarkupSafe = ">=0.9.2"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
i18n = ["Babel (>=2.7)"]
|
babel = ["Babel"]
|
||||||
|
lingua = ["lingua"]
|
||||||
|
testing = ["pytest"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
|
@ -444,6 +400,7 @@ version = "2.1.5"
|
||||||
description = "Safely add untrusted strings to HTML/XML markup."
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
||||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||||
|
@ -513,6 +470,7 @@ version = "1.0.0"
|
||||||
description = "Type system extensions for programs checked with the mypy type checker."
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
|
@ -524,6 +482,7 @@ version = "24.1"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||||
|
@ -535,36 +494,28 @@ version = "0.12.1"
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "peewee"
|
|
||||||
version = "3.17.6"
|
|
||||||
description = "a little orm"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "peewee-3.17.6.tar.gz", hash = "sha256:cea5592c6f4da1592b7cff8eaf655be6648a1f5857469e30037bf920c03fb8fb"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.2.2"
|
version = "4.3.6"
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
|
||||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
|
||||||
type = ["mypy (>=1.8)"]
|
type = ["mypy (>=1.11.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
|
@ -572,6 +523,7 @@ version = "1.5.0"
|
||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||||
|
@ -581,31 +533,22 @@ files = [
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycparser"
|
|
||||||
version = "2.22"
|
|
||||||
description = "C parser in Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
|
||||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.8.2"
|
version = "2.9.2"
|
||||||
description = "Data validation using Python type hints"
|
description = "Data validation using Python type hints"
|
||||||
optional = false
|
optional = true
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "extra == \"pydantic\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
|
||||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
annotated-types = ">=0.4.0"
|
annotated-types = ">=0.6.0"
|
||||||
pydantic-core = "2.20.1"
|
pydantic-core = "2.23.4"
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
||||||
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
||||||
|
@ -613,103 +556,106 @@ typing-extensions = [
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
email = ["email-validator (>=2.0.0)"]
|
email = ["email-validator (>=2.0.0)"]
|
||||||
|
timezone = ["tzdata ; python_version >= \"3.9\" and sys_platform == \"win32\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-core"
|
name = "pydantic-core"
|
||||||
version = "2.20.1"
|
version = "2.23.4"
|
||||||
description = "Core functionality for Pydantic validation and serialization"
|
description = "Core functionality for Pydantic validation and serialization"
|
||||||
optional = false
|
optional = true
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "extra == \"pydantic\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
|
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
|
{file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
|
{file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
|
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
|
{file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
|
||||||
{file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
|
{file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
|
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
|
{file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
|
||||||
{file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
|
{file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
|
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
|
{file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
|
||||||
{file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
|
{file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
|
{file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
|
{file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
|
||||||
{file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
|
{file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
|
{file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
|
{file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
|
||||||
{file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
|
{file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
|
||||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
|
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
|
||||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
|
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
|
||||||
{file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
|
{file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -717,13 +663,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.3.2"
|
version = "8.3.3"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -741,6 +688,7 @@ version = "0.5.7"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"},
|
{file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"},
|
||||||
{file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"},
|
{file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"},
|
||||||
|
@ -768,23 +716,124 @@ version = "2.4.0"
|
||||||
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
|
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
|
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
|
||||||
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
|
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy"
|
||||||
|
version = "2.0.44"
|
||||||
|
description = "Database Abstraction Library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "SQLAlchemy-2.0.44-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632"},
|
||||||
|
{file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f"},
|
||||||
|
{file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726"},
|
||||||
|
{file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1"},
|
||||||
|
{file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e"},
|
||||||
|
{file = "SQLAlchemy-2.0.44-cp37-cp37m-win32.whl", hash = "sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74"},
|
||||||
|
{file = "SQLAlchemy-2.0.44-cp37-cp37m-win_amd64.whl", hash = "sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-win32.whl", hash = "sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-win32.whl", hash = "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100"},
|
||||||
|
{file = "sqlalchemy-2.0.44-cp39-cp39-win_amd64.whl", hash = "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6"},
|
||||||
|
{file = "sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05"},
|
||||||
|
{file = "sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
||||||
|
typing-extensions = ">=4.6.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"]
|
||||||
|
aioodbc = ["aioodbc", "greenlet (>=1)"]
|
||||||
|
aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"]
|
||||||
|
asyncio = ["greenlet (>=1)"]
|
||||||
|
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"]
|
||||||
|
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
|
||||||
|
mssql = ["pyodbc"]
|
||||||
|
mssql-pymssql = ["pymssql"]
|
||||||
|
mssql-pyodbc = ["pyodbc"]
|
||||||
|
mypy = ["mypy (>=0.910)"]
|
||||||
|
mysql = ["mysqlclient (>=1.4.0)"]
|
||||||
|
mysql-connector = ["mysql-connector-python"]
|
||||||
|
oracle = ["cx_oracle (>=8)"]
|
||||||
|
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||||
|
postgresql = ["psycopg2 (>=2.7)"]
|
||||||
|
postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"]
|
||||||
|
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
|
||||||
|
postgresql-psycopg = ["psycopg (>=3.0.7)"]
|
||||||
|
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||||
|
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||||
|
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
|
||||||
|
pymysql = ["pymysql"]
|
||||||
|
sqlcipher = ["sqlcipher3_binary"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.12.2"
|
version = "4.12.2"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[extras]
|
||||||
|
pydantic = ["pydantic"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "17f5dc4b157da57ad75a6f6aa3feb7adfa07500b805d5e79d1f09d640964949f"
|
content-hash = "002b27684674c055d24144488bc7ad9f564b2788ab51d985c4b64fe1589b7ccb"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
[tool.poetry]
|
[project]
|
||||||
name = "spiderweb-framework"
|
name = "spiderweb-framework"
|
||||||
version = "1.2.1"
|
version = "2.0.0"
|
||||||
description = "A small web framework, just big enough for a spider."
|
description = "A small web framework, just big enough for a spider."
|
||||||
authors = ["Joe Kaufeld <opensource@joekaufeld.com>"]
|
authors = [{name="Joe Kaufeld", email="opensource@joekaufeld.com"}]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [{include = "spiderweb"}]
|
packages = [{include = "spiderweb"}]
|
||||||
license = "LICENSE.txt"
|
license = "MIT"
|
||||||
exclude = [
|
exclude = [
|
||||||
"tests/*",
|
"tests/*",
|
||||||
"example.py",
|
"example.py",
|
||||||
|
@ -27,14 +27,19 @@ classifiers = [
|
||||||
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
|
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
|
||||||
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
||||||
]
|
]
|
||||||
|
dependencies = [
|
||||||
|
"advanced-alchemy (>=1.6.3,<2.0.0)"
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
peewee = "^3.17.6"
|
SQLAlchemy = "^2.0.32"
|
||||||
jinja2 = "^3.1.4"
|
jinja2 = "^3.1.4"
|
||||||
cryptography = "^43.0.0"
|
cryptography = "^43.0.0"
|
||||||
email-validator = "^2.2.0"
|
email-validator = "^2.2.0"
|
||||||
pydantic = "^2.8.2"
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
pydantic = ["pydantic>=2.8.2,<3"]
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ruff = "^0.5.5"
|
ruff = "^0.5.5"
|
||||||
|
@ -45,8 +50,11 @@ hypothesis = "^6.111.2"
|
||||||
coverage = "^7.6.1"
|
coverage = "^7.6.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["hatchling"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["spiderweb/spiderweb-framework"]
|
||||||
|
|
||||||
[tool.poetry_bumpversion.file."spiderweb/constants.py"]
|
[tool.poetry_bumpversion.file."spiderweb/constants.py"]
|
||||||
|
|
||||||
|
@ -85,4 +93,4 @@ exclude_also = [
|
||||||
"if TYPE_CHECKING:",
|
"if TYPE_CHECKING:",
|
||||||
]
|
]
|
||||||
|
|
||||||
ignore_errors = true
|
ignore_errors = true
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
from peewee import DatabaseProxy
|
|
||||||
|
|
||||||
DEFAULT_ALLOWED_METHODS = ["POST", "GET", "PUT", "PATCH", "DELETE"]
|
DEFAULT_ALLOWED_METHODS = ["POST", "GET", "PUT", "PATCH", "DELETE"]
|
||||||
DEFAULT_ENCODING = "UTF-8"
|
DEFAULT_ENCODING = "UTF-8"
|
||||||
__version__ = "1.2.1"
|
__version__ = "1.3.1"
|
||||||
|
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||||
REGEX_COOKIE_NAME = r"^[a-zA-Z0-9\s\(\)<>@,;:\/\\\[\]\?=\{\}\"\t]*$"
|
REGEX_COOKIE_NAME = r"^[a-zA-Z0-9\s\(\)<>@,;:\/\\\[\]\?=\{\}\"\t]*$"
|
||||||
|
|
||||||
DATABASE_PROXY = DatabaseProxy()
|
|
||||||
|
|
||||||
DEFAULT_CORS_ALLOW_METHODS = (
|
DEFAULT_CORS_ALLOW_METHODS = (
|
||||||
"DELETE",
|
"DELETE",
|
||||||
"GET",
|
"GET",
|
||||||
|
|
119
spiderweb/db.py
119
spiderweb/db.py
|
@ -1,101 +1,28 @@
|
||||||
from peewee import Model, Field, SchemaManager
|
from __future__ import annotations
|
||||||
|
|
||||||
from spiderweb.constants import DATABASE_PROXY
|
from pathlib import Path
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy.orm import declarative_base, sessionmaker, Session as SASession
|
||||||
|
|
||||||
|
# Base class for SQLAlchemy models used internally by Spiderweb
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
# Type alias for sessions
|
||||||
|
DBSession = SASession
|
||||||
|
|
||||||
|
|
||||||
class MigrationsNeeded(ExceptionGroup): ...
|
def create_sqlite_engine(db_path: Union[str, Path]) -> Engine:
|
||||||
|
"""Create a SQLite engine from a file path."""
|
||||||
|
path = Path(db_path)
|
||||||
|
# Ensure directory exists
|
||||||
|
if path.parent and not path.parent.exists():
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
return create_engine(f"sqlite:///{path}", future=True)
|
||||||
|
|
||||||
|
|
||||||
class MigrationRequired(Exception): ...
|
def create_session_factory(engine: Engine):
|
||||||
|
"""Return a configured sessionmaker bound to the given engine."""
|
||||||
|
return sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
|
||||||
class SpiderwebModel(Model):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def check_for_needed_migration(cls):
|
|
||||||
if hasattr(cls._meta, "skip_migration_check"):
|
|
||||||
return
|
|
||||||
|
|
||||||
current_model_fields: dict[str, Field] = cls._meta.fields
|
|
||||||
current_db_fields = {
|
|
||||||
c.name: {
|
|
||||||
"data_type": c.data_type,
|
|
||||||
"null": c.null,
|
|
||||||
"primary_key": c.primary_key,
|
|
||||||
"default": c.default,
|
|
||||||
}
|
|
||||||
for c in cls._meta.database.get_columns(cls._meta.table_name)
|
|
||||||
}
|
|
||||||
problems = []
|
|
||||||
s = SchemaManager(cls, cls._meta.database)
|
|
||||||
ctx = s._create_context()
|
|
||||||
for field_name, field_obj in current_model_fields.items():
|
|
||||||
db_version = current_db_fields.get(field_obj.column_name)
|
|
||||||
if not db_version:
|
|
||||||
problems.append(
|
|
||||||
MigrationRequired(f"Field {field_name} not found in DB.")
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if field_obj.field_type == "VARCHAR":
|
|
||||||
field_obj.max_length = field_obj.max_length or 255
|
|
||||||
if (
|
|
||||||
cls._meta.fields[field_name].ddl_datatype(ctx).sql
|
|
||||||
!= db_version["data_type"]
|
|
||||||
):
|
|
||||||
problems.append(
|
|
||||||
MigrationRequired(
|
|
||||||
f"CharField `{field_name}` has changed the field type."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if (
|
|
||||||
cls._meta.database.get_context_options()["field_types"][
|
|
||||||
field_obj.field_type
|
|
||||||
]
|
|
||||||
!= db_version["data_type"]
|
|
||||||
):
|
|
||||||
problems.append(
|
|
||||||
MigrationRequired(
|
|
||||||
f"Field `{field_name}` has changed the field type."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if field_obj.null != db_version["null"]:
|
|
||||||
problems.append(
|
|
||||||
MigrationRequired(
|
|
||||||
f"Field `{field_name}` has changed the nullability."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if field_obj.__class__.__name__ == "BooleanField":
|
|
||||||
if field_obj.default is False and db_version["default"] not in (
|
|
||||||
False,
|
|
||||||
None,
|
|
||||||
0,
|
|
||||||
):
|
|
||||||
problems.append(
|
|
||||||
MigrationRequired(
|
|
||||||
f"BooleanField `{field_name}` has changed the default value."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif field_obj.default is True and db_version["default"] not in (
|
|
||||||
True,
|
|
||||||
1,
|
|
||||||
):
|
|
||||||
problems.append(
|
|
||||||
MigrationRequired(
|
|
||||||
f"BooleanField `{field_name}` has changed the default value."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if field_obj.default != db_version["default"]:
|
|
||||||
problems.append(
|
|
||||||
MigrationRequired(
|
|
||||||
f"Field `{field_name}` has changed the default value."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if problems:
|
|
||||||
raise MigrationsNeeded(f"The model {cls} requires migrations.", problems)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
database = DATABASE_PROXY
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ class LocalServerMixin:
|
||||||
|
|
||||||
def start(self, blocking=False):
|
def start(self, blocking=False):
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
self.log.info(f"Starting server on {self.addr}:{self.port}")
|
self.log.info(f"Starting server on http://{self.addr}:{self.port}")
|
||||||
self.log.info("Press CTRL+C to stop the server.")
|
self.log.info("Press CTRL+C to stop the server.")
|
||||||
self._server = self.create_server()
|
self._server = self.create_server()
|
||||||
self._thread = threading.Thread(target=self._server.serve_forever)
|
self._thread = threading.Thread(target=self._server.serve_forever)
|
||||||
|
|
|
@ -10,7 +10,8 @@ from typing import Optional, Callable, Sequence, Literal
|
||||||
from wsgiref.simple_server import WSGIServer
|
from wsgiref.simple_server import WSGIServer
|
||||||
|
|
||||||
from jinja2 import BaseLoader, FileSystemLoader
|
from jinja2 import BaseLoader, FileSystemLoader
|
||||||
from peewee import Database, SqliteDatabase
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
from spiderweb.middleware import MiddlewareMixin
|
from spiderweb.middleware import MiddlewareMixin
|
||||||
from spiderweb.constants import (
|
from spiderweb.constants import (
|
||||||
|
@ -18,11 +19,10 @@ from spiderweb.constants import (
|
||||||
DEFAULT_CORS_ALLOW_HEADERS,
|
DEFAULT_CORS_ALLOW_HEADERS,
|
||||||
)
|
)
|
||||||
from spiderweb.constants import (
|
from spiderweb.constants import (
|
||||||
DATABASE_PROXY,
|
|
||||||
DEFAULT_ENCODING,
|
DEFAULT_ENCODING,
|
||||||
DEFAULT_ALLOWED_METHODS,
|
DEFAULT_ALLOWED_METHODS,
|
||||||
)
|
)
|
||||||
from spiderweb.db import SpiderwebModel
|
from spiderweb.db import Base, create_sqlite_engine, create_session_factory
|
||||||
from spiderweb.default_views import (
|
from spiderweb.default_views import (
|
||||||
http403, # noqa: F401
|
http403, # noqa: F401
|
||||||
http404, # noqa: F401
|
http404, # noqa: F401
|
||||||
|
@ -67,8 +67,10 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
||||||
cors_allow_credentials: bool = False,
|
cors_allow_credentials: bool = False,
|
||||||
cors_allow_private_network: bool = False,
|
cors_allow_private_network: bool = False,
|
||||||
csrf_trusted_origins: Sequence[str] = None,
|
csrf_trusted_origins: Sequence[str] = None,
|
||||||
db: Optional[Database] = None,
|
db: Optional[Engine | str] = None,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
|
gzip_compression_level: int = 6,
|
||||||
|
gzip_minimum_response_length: int = 500,
|
||||||
templates_dirs: Sequence[str] = None,
|
templates_dirs: Sequence[str] = None,
|
||||||
middleware: Sequence[str] = None,
|
middleware: Sequence[str] = None,
|
||||||
append_slash: bool = False,
|
append_slash: bool = False,
|
||||||
|
@ -119,6 +121,9 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
||||||
convert_url_to_regex(i) for i in self._csrf_trusted_origins
|
convert_url_to_regex(i) for i in self._csrf_trusted_origins
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.gzip_compression_level = gzip_compression_level
|
||||||
|
self.gzip_minimum_response_length = gzip_minimum_response_length
|
||||||
|
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
self.extra_data = kwargs
|
self.extra_data = kwargs
|
||||||
|
@ -143,12 +148,21 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
||||||
self.init_fernet()
|
self.init_fernet()
|
||||||
self.init_middleware()
|
self.init_middleware()
|
||||||
|
|
||||||
self.db = db or SqliteDatabase(self.BASE_DIR / "spiderweb.db")
|
# Database setup (SQLAlchemy)
|
||||||
# give the models the db connection
|
if isinstance(db, Engine):
|
||||||
DATABASE_PROXY.initialize(self.db)
|
self.db_engine = db
|
||||||
self.db.create_tables(SpiderwebModel.__subclasses__())
|
elif isinstance(db, str):
|
||||||
for model in SpiderwebModel.__subclasses__():
|
# treat as URL if it looks like one, otherwise as a filesystem path
|
||||||
model.check_for_needed_migration()
|
if "://" in db:
|
||||||
|
self.db_engine = create_engine(db, future=True)
|
||||||
|
else:
|
||||||
|
self.db_engine = create_sqlite_engine(self.BASE_DIR / db)
|
||||||
|
else:
|
||||||
|
self.db_engine = create_sqlite_engine(self.BASE_DIR / "spiderweb.db")
|
||||||
|
|
||||||
|
self.db_session_factory = create_session_factory(self.db_engine)
|
||||||
|
# Create internal tables (e.g., sessions)
|
||||||
|
Base.metadata.create_all(self.db_engine)
|
||||||
|
|
||||||
if self.routes:
|
if self.routes:
|
||||||
self.add_routes()
|
self.add_routes()
|
||||||
|
@ -201,6 +215,17 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
||||||
|
|
||||||
def fire_response(self, start_response, request: Request, resp: HttpResponse):
|
def fire_response(self, start_response, request: Request, resp: HttpResponse):
|
||||||
try:
|
try:
|
||||||
|
try:
|
||||||
|
rendered_output: str = resp.render()
|
||||||
|
final_output: str | list[str] = self.post_process_middleware(
|
||||||
|
request, resp, rendered_output
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Fatal error!")
|
||||||
|
self.log.error(e)
|
||||||
|
self.log.error(traceback.format_exc())
|
||||||
|
return [f"Internal Server Error: {e}".encode(DEFAULT_ENCODING)]
|
||||||
|
|
||||||
status = get_http_status_by_code(resp.status_code)
|
status = get_http_status_by_code(resp.status_code)
|
||||||
cookies = []
|
cookies = []
|
||||||
varies = []
|
varies = []
|
||||||
|
@ -218,23 +243,13 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
||||||
for v in varies:
|
for v in varies:
|
||||||
headers.append(("vary", str(v)))
|
headers.append(("vary", str(v)))
|
||||||
|
|
||||||
start_response(status, headers)
|
if not isinstance(final_output, list):
|
||||||
|
final_output = [final_output]
|
||||||
try:
|
|
||||||
rendered_output = resp.render()
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error("Fatal error!")
|
|
||||||
self.log.error(e)
|
|
||||||
self.log.error(traceback.format_exc())
|
|
||||||
return [f"Internal Server Error: {e}".encode(DEFAULT_ENCODING)]
|
|
||||||
|
|
||||||
if not isinstance(rendered_output, list):
|
|
||||||
rendered_output = [rendered_output]
|
|
||||||
encoded_resp = [
|
encoded_resp = [
|
||||||
chunk.encode(DEFAULT_ENCODING) if isinstance(chunk, str) else chunk
|
chunk.encode(DEFAULT_ENCODING) if isinstance(chunk, str) else chunk
|
||||||
for chunk in rendered_output
|
for chunk in final_output
|
||||||
]
|
]
|
||||||
|
start_response(status, headers)
|
||||||
return encoded_resp
|
return encoded_resp
|
||||||
except APIError:
|
except APIError:
|
||||||
raise
|
raise
|
||||||
|
@ -259,6 +274,10 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
||||||
server=self,
|
server=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_db_session(self):
|
||||||
|
"""Return a new SQLAlchemy session bound to the application's engine."""
|
||||||
|
return self.db_session_factory()
|
||||||
|
|
||||||
def send_error_response(
|
def send_error_response(
|
||||||
self, start_response, request: Request, e: SpiderwebNetworkException
|
self, start_response, request: Request, e: SpiderwebNetworkException
|
||||||
):
|
):
|
||||||
|
|
|
@ -60,3 +60,18 @@ class MiddlewareMixin:
|
||||||
except UnusedMiddleware:
|
except UnusedMiddleware:
|
||||||
self.middleware.remove(middleware)
|
self.middleware.remove(middleware)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
def post_process_middleware(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str | bytes:
|
||||||
|
# run them in reverse order, same as process_response. The top of the middleware
|
||||||
|
# stack should be the first and last middleware to run.
|
||||||
|
for middleware in reversed(self.middleware):
|
||||||
|
try:
|
||||||
|
rendered_response = middleware.post_process(
|
||||||
|
request, response, rendered_response
|
||||||
|
)
|
||||||
|
except UnusedMiddleware:
|
||||||
|
self.middleware.remove(middleware)
|
||||||
|
continue
|
||||||
|
return rendered_response
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from spiderweb.request import Request
|
from spiderweb.request import Request
|
||||||
from spiderweb.response import HttpResponse
|
from spiderweb.response import HttpResponse
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from spiderweb.server_checks import ServerCheck
|
||||||
|
|
||||||
|
|
||||||
class SpiderwebMiddleware:
|
class SpiderwebMiddleware:
|
||||||
"""
|
"""
|
||||||
|
@ -9,6 +14,8 @@ class SpiderwebMiddleware:
|
||||||
|
|
||||||
process_request(self, request) -> None or Response
|
process_request(self, request) -> None or Response
|
||||||
process_response(self, request, resp) -> None
|
process_response(self, request, resp) -> None
|
||||||
|
on_error(self, request, e) -> Response
|
||||||
|
post_process(self, request, resp) -> Response
|
||||||
|
|
||||||
Middleware can be used to modify requests and responses in a variety of ways.
|
Middleware can be used to modify requests and responses in a variety of ways.
|
||||||
If one of the two methods is not defined, the request or resp will be passed
|
If one of the two methods is not defined, the request or resp will be passed
|
||||||
|
@ -20,14 +27,33 @@ class SpiderwebMiddleware:
|
||||||
|
|
||||||
def __init__(self, server):
|
def __init__(self, server):
|
||||||
self.server = server
|
self.server = server
|
||||||
|
# If there are any startup checks that need to be run, they should be added
|
||||||
|
# to this list. These checks should be classes that inherit from
|
||||||
|
# spiderweb.server_checks.ServerCheck.
|
||||||
|
self.checks: list[ServerCheck]
|
||||||
|
|
||||||
def process_request(self, request: Request) -> HttpResponse | None:
|
def process_request(self, request: Request) -> HttpResponse | None:
|
||||||
|
# This method is called before the request is passed to the view. You can safely
|
||||||
|
# modify the request in this method, or return an HttpResponse to short-circuit
|
||||||
|
# the request and return a response immediately.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def process_response(
|
def process_response(self, request: Request, response: HttpResponse) -> None:
|
||||||
self, request: Request, response: HttpResponse
|
# This method is called after the view has returned a response. You can modify
|
||||||
) -> HttpResponse | None:
|
# the response in this method. The response will be returned to the client after
|
||||||
|
# all middleware has been processed.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_error(self, request: Request, e: Exception) -> HttpResponse | None:
|
def on_error(self, request: Request, e: Exception) -> HttpResponse | None:
|
||||||
|
# This method is called if an exception is raised during the request. You can
|
||||||
|
# return a response here to handle the error. If you return None, the exception
|
||||||
|
# will be re-raised.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
# This method is called after all the middleware has been processed and receives
|
||||||
|
# the final rendered response in str form. You can modify the response here. This
|
||||||
|
# method *must* return a str version of the rendered response.
|
||||||
|
return rendered_response
|
||||||
|
|
|
@ -13,7 +13,9 @@ from spiderweb.server_checks import ServerCheck
|
||||||
class CheckForSessionMiddleware(ServerCheck):
|
class CheckForSessionMiddleware(ServerCheck):
|
||||||
SESSION_MIDDLEWARE_NOT_FOUND = (
|
SESSION_MIDDLEWARE_NOT_FOUND = (
|
||||||
"Session middleware is not enabled. It must be listed above"
|
"Session middleware is not enabled. It must be listed above"
|
||||||
"CSRFMiddleware in the middleware list."
|
"CSRFMiddleware in the middleware list. Add"
|
||||||
|
" 'spiderweb.middleware.sessions.SessionMiddleware' to your"
|
||||||
|
" `middleware` list."
|
||||||
)
|
)
|
||||||
|
|
||||||
def check(self) -> Optional[Exception]:
|
def check(self) -> Optional[Exception]:
|
||||||
|
@ -26,8 +28,8 @@ class CheckForSessionMiddleware(ServerCheck):
|
||||||
|
|
||||||
class VerifyCorrectMiddlewarePlacement(ServerCheck):
|
class VerifyCorrectMiddlewarePlacement(ServerCheck):
|
||||||
SESSION_MIDDLEWARE_BELOW_CSRF = (
|
SESSION_MIDDLEWARE_BELOW_CSRF = (
|
||||||
"SessionMiddleware is enabled, but it must be listed above"
|
"Session middleware is enabled, but must be listed above"
|
||||||
"CSRFMiddleware in the middleware list."
|
" CSRFMiddleware in the middleware list."
|
||||||
)
|
)
|
||||||
|
|
||||||
def check(self) -> Optional[Exception]:
|
def check(self) -> Optional[Exception]:
|
||||||
|
|
70
spiderweb/middleware/gzip.py
Normal file
70
spiderweb/middleware/gzip.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""
|
||||||
|
Source code inspiration: https://github.com/colour-science/flask-compress/blob/master/flask_compress/flask_compress.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from spiderweb.exceptions import ConfigError
|
||||||
|
from spiderweb.middleware import SpiderwebMiddleware
|
||||||
|
from spiderweb.server_checks import ServerCheck
|
||||||
|
from spiderweb.request import Request
|
||||||
|
from spiderweb.response import HttpResponse
|
||||||
|
|
||||||
|
import gzip
|
||||||
|
|
||||||
|
|
||||||
|
class CheckValidGzipCompressionLevel(ServerCheck):
|
||||||
|
INVALID_GZIP_COMPRESSION_LEVEL = (
|
||||||
|
"`gzip_compression_level` must be an integer between 1 and 9."
|
||||||
|
)
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
if not isinstance(self.server.gzip_compression_level, int):
|
||||||
|
raise ConfigError(self.INVALID_GZIP_COMPRESSION_LEVEL)
|
||||||
|
if self.server.gzip_compression_level not in range(1, 10):
|
||||||
|
raise ConfigError(
|
||||||
|
"Gzip compression level must be an integer between 1 and 9."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckValidGzipMinimumLength(ServerCheck):
|
||||||
|
INVALID_GZIP_MINIMUM_LENGTH = "`gzip_minimum_length` must be a positive integer."
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
if not isinstance(self.server.gzip_minimum_response_length, int):
|
||||||
|
raise ConfigError(self.INVALID_GZIP_MINIMUM_LENGTH)
|
||||||
|
if self.server.gzip_minimum_response_length < 1:
|
||||||
|
raise ConfigError(self.INVALID_GZIP_MINIMUM_LENGTH)
|
||||||
|
|
||||||
|
|
||||||
|
class GzipMiddleware(SpiderwebMiddleware):
|
||||||
|
|
||||||
|
checks = [CheckValidGzipCompressionLevel, CheckValidGzipMinimumLength]
|
||||||
|
|
||||||
|
algorithm = "gzip"
|
||||||
|
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str | bytes:
|
||||||
|
# Only actually compress the response if the following attributes are true:
|
||||||
|
#
|
||||||
|
# - The response status code is a 2xx success code
|
||||||
|
# - The response length is at least 500 bytes
|
||||||
|
# - The response is not a streaming response
|
||||||
|
# - (already bytes, like from FileResponse)
|
||||||
|
# - The response is not already compressed
|
||||||
|
# - The request accepts gzip encoding
|
||||||
|
if (
|
||||||
|
not (200 <= response.status_code < 300)
|
||||||
|
or len(rendered_response) < self.server.gzip_minimum_response_length
|
||||||
|
or not isinstance(rendered_response, str)
|
||||||
|
or self.algorithm in response.headers.get("Content-Encoding", "")
|
||||||
|
or self.algorithm not in request.headers.get("Accept-Encoding", "")
|
||||||
|
):
|
||||||
|
return rendered_response
|
||||||
|
|
||||||
|
zipped = gzip.compress(
|
||||||
|
rendered_response.encode("UTF-8"),
|
||||||
|
compresslevel=self.server.gzip_compression_level,
|
||||||
|
)
|
||||||
|
response.headers["Content-Encoding"] = self.algorithm
|
||||||
|
response.headers["Content-Length"] = str(len(zipped))
|
||||||
|
return zipped
|
|
@ -1,8 +1,26 @@
|
||||||
import inspect
|
import inspect
|
||||||
from typing import get_type_hints
|
from typing import get_type_hints
|
||||||
|
|
||||||
from pydantic import BaseModel
|
try: # pragma: no cover - import guard
|
||||||
from pydantic_core._pydantic_core import ValidationError
|
from pydantic import BaseModel # type: ignore
|
||||||
|
from pydantic_core._pydantic_core import ValidationError # type: ignore
|
||||||
|
PYDANTIC_AVAILABLE = True
|
||||||
|
except Exception: # pragma: no cover - executed only when pydantic isn't installed
|
||||||
|
PYDANTIC_AVAILABLE = False
|
||||||
|
|
||||||
|
class BaseModel: # minimal stub to allow module import without pydantic
|
||||||
|
@classmethod
|
||||||
|
def parse_obj(cls, *args, **kwargs): # noqa: D401 - simple shim
|
||||||
|
raise RuntimeError(
|
||||||
|
"Pydantic is not installed. Install with 'pip install"
|
||||||
|
" spiderweb-framework[pydantic]' or 'pip install pydantic'"
|
||||||
|
" to use PydanticMiddleware."
|
||||||
|
)
|
||||||
|
|
||||||
|
class ValidationError(Exception): # simple stand-in so type hints resolve
|
||||||
|
def errors(self): # match pydantic's ValidationError API used below
|
||||||
|
return []
|
||||||
|
|
||||||
from spiderweb import SpiderwebMiddleware
|
from spiderweb import SpiderwebMiddleware
|
||||||
from spiderweb.request import Request
|
from spiderweb.request import Request
|
||||||
from spiderweb.response import JsonResponse
|
from spiderweb.response import JsonResponse
|
||||||
|
@ -19,6 +37,12 @@ class PydanticMiddleware(SpiderwebMiddleware):
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
if not request.method == "POST":
|
if not request.method == "POST":
|
||||||
return
|
return
|
||||||
|
if not PYDANTIC_AVAILABLE:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Pydantic is not installed. Install with 'pip install"
|
||||||
|
" spiderweb-framework[pydantic]' or 'pip install pydantic'"
|
||||||
|
" to use PydanticMiddleware."
|
||||||
|
)
|
||||||
types = get_type_hints(request.handler)
|
types = get_type_hints(request.handler)
|
||||||
# we don't know what the user named the request object, but
|
# we don't know what the user named the request object, but
|
||||||
# we know that it's first in the list, and it's always an arg.
|
# we know that it's first in the list, and it's always an arg.
|
||||||
|
@ -34,7 +58,15 @@ class PydanticMiddleware(SpiderwebMiddleware):
|
||||||
# Separated out into its own method so that it can be overridden
|
# Separated out into its own method so that it can be overridden
|
||||||
errors = e.errors()
|
errors = e.errors()
|
||||||
error_dict = {"message": "Validation error", "errors": []}
|
error_dict = {"message": "Validation error", "errors": []}
|
||||||
# [{'type': 'missing', 'loc': ('comment',), 'msg': 'Field required', 'input': {'email': 'a@a.com'}, 'url': 'https://errors.pydantic.dev/2.8/v/missing'}]
|
# [
|
||||||
|
# {
|
||||||
|
# 'type': 'missing',
|
||||||
|
# 'loc': ('comment',),
|
||||||
|
# 'msg': 'Field required',
|
||||||
|
# 'input': {'email': 'a@a.com'},
|
||||||
|
# 'url': 'https://errors.pydantic.dev/2.8/v/missing'
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
for error in errors:
|
for error in errors:
|
||||||
field = error["loc"][0]
|
field = error["loc"][0]
|
||||||
msg = error["msg"]
|
msg = error["msg"]
|
||||||
|
|
|
@ -1,61 +1,71 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from peewee import CharField, TextField, DateTimeField
|
from sqlalchemy import Column, Integer, String, Text, DateTime
|
||||||
|
from sqlalchemy.orm import Mapped
|
||||||
|
|
||||||
from spiderweb.middleware import SpiderwebMiddleware
|
from spiderweb.middleware import SpiderwebMiddleware
|
||||||
from spiderweb.request import Request
|
from spiderweb.request import Request
|
||||||
from spiderweb.response import HttpResponse
|
from spiderweb.response import HttpResponse
|
||||||
from spiderweb.db import SpiderwebModel
|
from spiderweb.db import Base
|
||||||
from spiderweb.utils import generate_key, is_jsonable
|
from spiderweb.utils import generate_key, is_jsonable
|
||||||
|
|
||||||
|
|
||||||
class Session(SpiderwebModel):
|
class Session(Base):
|
||||||
session_key = CharField(max_length=64)
|
__tablename__ = "spiderweb_sessions"
|
||||||
csrf_token = CharField(max_length=64, null=True)
|
|
||||||
user_id = CharField(max_length=64, null=True)
|
|
||||||
session_data = TextField()
|
|
||||||
created_at = DateTimeField()
|
|
||||||
last_active = DateTimeField()
|
|
||||||
ip_address = CharField(max_length=30)
|
|
||||||
user_agent = TextField()
|
|
||||||
|
|
||||||
class Meta:
|
id: Mapped[int] = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
table_name = "spiderweb_sessions"
|
session_key: Mapped[str] = Column(String(64), index=True, nullable=False)
|
||||||
|
csrf_token: Mapped[str | None] = Column(String(64), nullable=True)
|
||||||
|
user_id: Mapped[str | None] = Column(String(64), nullable=True)
|
||||||
|
session_data: Mapped[str] = Column(Text, nullable=False)
|
||||||
|
created_at: Mapped[datetime] = Column(DateTime, nullable=False)
|
||||||
|
last_active: Mapped[datetime] = Column(DateTime, nullable=False)
|
||||||
|
ip_address: Mapped[str] = Column(String(30), nullable=False)
|
||||||
|
user_agent: Mapped[str] = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class SessionMiddleware(SpiderwebMiddleware):
|
class SessionMiddleware(SpiderwebMiddleware):
|
||||||
def process_request(self, request: Request):
|
def process_request(self, request: Request):
|
||||||
existing_session = (
|
dbsession = self.server.get_db_session()
|
||||||
Session.select()
|
try:
|
||||||
.where(
|
existing_session = (
|
||||||
Session.session_key
|
dbsession.query(Session)
|
||||||
== request.COOKIES.get(self.server.session_cookie_name),
|
.filter(
|
||||||
Session.ip_address == request.META.get("client_address"),
|
Session.session_key
|
||||||
Session.user_agent == request.headers.get("HTTP_USER_AGENT"),
|
== request.COOKIES.get(self.server.session_cookie_name),
|
||||||
|
Session.ip_address == request.META.get("client_address"),
|
||||||
|
Session.user_agent == request.headers.get("HTTP_USER_AGENT"),
|
||||||
|
)
|
||||||
|
.order_by(Session.id.desc())
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
.first()
|
new_session = False
|
||||||
)
|
if not existing_session:
|
||||||
new_session = False
|
new_session = True
|
||||||
if not existing_session:
|
elif datetime.now() - existing_session.created_at > timedelta(
|
||||||
new_session = True
|
seconds=self.server.session_max_age
|
||||||
elif datetime.now() - existing_session.created_at > timedelta(
|
):
|
||||||
seconds=self.server.session_max_age
|
dbsession.delete(existing_session)
|
||||||
):
|
dbsession.commit()
|
||||||
existing_session.delete_instance()
|
new_session = True
|
||||||
new_session = True
|
|
||||||
|
|
||||||
if new_session:
|
if new_session:
|
||||||
request.SESSION = {}
|
request.SESSION = {}
|
||||||
request._session["id"] = generate_key()
|
request._session["id"] = generate_key()
|
||||||
request._session["new_session"] = True
|
request._session["new_session"] = True
|
||||||
request.META["SESSION"] = None
|
request.META["SESSION"] = None
|
||||||
return
|
return
|
||||||
|
|
||||||
request.SESSION = json.loads(existing_session.session_data)
|
request.SESSION = json.loads(existing_session.session_data)
|
||||||
request.META["SESSION"] = existing_session
|
request.META["SESSION"] = existing_session
|
||||||
request._session["id"] = existing_session.session_key
|
request._session["id"] = existing_session.session_key
|
||||||
existing_session.save()
|
# touch last_active
|
||||||
|
existing_session.last_active = datetime.now()
|
||||||
|
dbsession.add(existing_session)
|
||||||
|
dbsession.commit()
|
||||||
|
finally:
|
||||||
|
dbsession.close()
|
||||||
|
|
||||||
def process_response(self, request: Request, response: HttpResponse):
|
def process_response(self, request: Request, response: HttpResponse):
|
||||||
cookie_settings = {
|
cookie_settings = {
|
||||||
|
@ -78,19 +88,25 @@ class SessionMiddleware(SpiderwebMiddleware):
|
||||||
)
|
)
|
||||||
if not is_jsonable(request.SESSION):
|
if not is_jsonable(request.SESSION):
|
||||||
raise ValueError("Session data is not JSON serializable.")
|
raise ValueError("Session data is not JSON serializable.")
|
||||||
session = Session(
|
dbsession = self.server.get_db_session()
|
||||||
session_key=session_key,
|
try:
|
||||||
session_data=json.dumps(request.SESSION),
|
session = Session(
|
||||||
created_at=datetime.now(),
|
session_key=session_key,
|
||||||
last_active=datetime.now(),
|
session_data=json.dumps(request.SESSION),
|
||||||
ip_address=request.META.get("client_address"),
|
created_at=datetime.now(),
|
||||||
user_agent=request.headers.get("HTTP_USER_AGENT"),
|
last_active=datetime.now(),
|
||||||
)
|
ip_address=request.META.get("client_address"),
|
||||||
session.save()
|
user_agent=request.headers.get("HTTP_USER_AGENT"),
|
||||||
|
)
|
||||||
|
dbsession.add(session)
|
||||||
|
dbsession.commit()
|
||||||
|
finally:
|
||||||
|
dbsession.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Otherwise, we can save the one we already have.
|
# Otherwise, we can save the one we already have.
|
||||||
session_key = request.META["SESSION"].session_key
|
# Use the cached session id to avoid touching a detached SQLAlchemy instance.
|
||||||
|
session_key = request._session["id"]
|
||||||
# update the session expiration time
|
# update the session expiration time
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
self.server.session_cookie_name,
|
self.server.session_cookie_name,
|
||||||
|
@ -98,7 +114,18 @@ class SessionMiddleware(SpiderwebMiddleware):
|
||||||
**cookie_settings,
|
**cookie_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
session = request.META["SESSION"]
|
dbsession = self.server.get_db_session()
|
||||||
session.session_data = json.dumps(request.SESSION)
|
try:
|
||||||
session.last_active = datetime.now()
|
session = (
|
||||||
session.save()
|
dbsession.query(Session)
|
||||||
|
.filter(Session.session_key == session_key)
|
||||||
|
.order_by(Session.id.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if session:
|
||||||
|
session.session_data = json.dumps(request.SESSION)
|
||||||
|
session.last_active = datetime.now()
|
||||||
|
dbsession.add(session)
|
||||||
|
dbsession.commit()
|
||||||
|
finally:
|
||||||
|
dbsession.close()
|
||||||
|
|
|
@ -80,11 +80,21 @@ class Request:
|
||||||
self.META["client_address"] = get_client_address(self.environ)
|
self.META["client_address"] = get_client_address(self.environ)
|
||||||
|
|
||||||
def populate_cookies(self) -> None:
|
def populate_cookies(self) -> None:
|
||||||
if cookies := self.environ.get("HTTP_COOKIE"):
|
cookies_header = self.environ.get("HTTP_COOKIE")
|
||||||
self.COOKIES = {
|
if not cookies_header:
|
||||||
option.split("=")[0]: option.split("=")[1]
|
return
|
||||||
for option in cookies.split("; ")
|
cookies: dict[str, str] = {}
|
||||||
}
|
# Split on ';' and be tolerant of optional spaces and malformed segments
|
||||||
|
for segment in cookies_header.split(";"):
|
||||||
|
part = segment.strip()
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
if "=" not in part:
|
||||||
|
# Ignore flag-like segments that don't conform to name=value
|
||||||
|
continue
|
||||||
|
name, _, value = part.partition("=") # only split on first '='
|
||||||
|
cookies[name.strip()] = value.strip()
|
||||||
|
self.COOKIES = cookies
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
return json.loads(self.content)
|
return json.loads(self.content)
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
from wsgiref.util import setup_testing_defaults
|
from wsgiref.util import setup_testing_defaults
|
||||||
|
|
||||||
from peewee import SqliteDatabase
|
|
||||||
|
|
||||||
from spiderweb import SpiderwebRouter
|
from spiderweb import SpiderwebRouter
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,7 +20,7 @@ def setup(**kwargs):
|
||||||
environ = {}
|
environ = {}
|
||||||
setup_testing_defaults(environ)
|
setup_testing_defaults(environ)
|
||||||
if "db" not in kwargs:
|
if "db" not in kwargs:
|
||||||
kwargs["db"] = SqliteDatabase("spiderweb-tests.db")
|
kwargs["db"] = "spiderweb-tests.db"
|
||||||
return (
|
return (
|
||||||
SpiderwebRouter(**kwargs),
|
SpiderwebRouter(**kwargs),
|
||||||
environ,
|
environ,
|
||||||
|
|
|
@ -16,3 +16,25 @@ class ExplodingResponseMiddleware(SpiderwebMiddleware):
|
||||||
class InterruptingMiddleware(SpiderwebMiddleware):
|
class InterruptingMiddleware(SpiderwebMiddleware):
|
||||||
def process_request(self, request: Request) -> HttpResponse:
|
def process_request(self, request: Request) -> HttpResponse:
|
||||||
return HttpResponse("Moo!")
|
return HttpResponse("Moo!")
|
||||||
|
|
||||||
|
|
||||||
|
class PostProcessingMiddleware(SpiderwebMiddleware):
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
return rendered_response + " Moo!"
|
||||||
|
|
||||||
|
|
||||||
|
class PostProcessingWithHeaderManipulation(SpiderwebMiddleware):
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
response.headers["X-Moo"] = "true"
|
||||||
|
return rendered_response
|
||||||
|
|
||||||
|
|
||||||
|
class ExplodingPostProcessingMiddleware(SpiderwebMiddleware):
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
raise UnusedMiddleware("Unfinished!")
|
||||||
|
|
|
@ -147,3 +147,71 @@ def test_setting_multiple_cookies():
|
||||||
app(environ, start_response)
|
app(environ, start_response)
|
||||||
assert start_response.headers[-1] == ("set-cookie", "cookie2=value2")
|
assert start_response.headers[-1] == ("set-cookie", "cookie2=value2")
|
||||||
assert start_response.headers[-2] == ("set-cookie", "cookie1=value1")
|
assert start_response.headers[-2] == ("set-cookie", "cookie1=value1")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cookie_header,expected",
|
||||||
|
[
|
||||||
|
("", {}),
|
||||||
|
(" ", {}),
|
||||||
|
(";", {}),
|
||||||
|
(";; ; ", {}),
|
||||||
|
("a=1", {"a": "1"}),
|
||||||
|
("a=1; b=2", {"a": "1", "b": "2"}),
|
||||||
|
("a=1; b", {"a": "1"}), # flag-like segment ignored
|
||||||
|
("flag", {}), # single flag ignored
|
||||||
|
("a=1; flag; c=3", {"a": "1", "c": "3"}),
|
||||||
|
("a=1; c=", {"a": "1", "c": ""}), # empty value allowed
|
||||||
|
("token=abc=def==", {"token": "abc=def=="}), # values may contain '='
|
||||||
|
(" d = q ", {"d": "q"}), # tolerate spaces around name/value
|
||||||
|
("a=1; ; ; c=3", {"a": "1", "c": "3"}), # empty segments ignored
|
||||||
|
("a=1; a=2", {"a": "2"}), # last duplicate wins
|
||||||
|
("q=\"a b c\"", {"q": '"a b c"'}), # quotes preserved
|
||||||
|
("u=hello%3Dworld", {"u": "hello%3Dworld"}), # url-encoded preserved
|
||||||
|
("=novalue; a=1", {"": "novalue", "a": "1"}), # empty name retained per current parser
|
||||||
|
("lead=1; ; trail=2;", {"lead": "1", "trail": "2"}),
|
||||||
|
(" spaced = value ; another= thing ", {"spaced": "value", "another": "thing"}),
|
||||||
|
("a=1; b=2; flag; c=; token=abc=def==; d = q ; ;", {"a": "1", "b": "2", "c": "", "token": "abc=def==", "d": "q"}),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"empty",
|
||||||
|
"space-only",
|
||||||
|
"single-semicolon",
|
||||||
|
"many-empty",
|
||||||
|
"single-pair",
|
||||||
|
"two-pairs",
|
||||||
|
"flag-after",
|
||||||
|
"single-flag",
|
||||||
|
"mix-flag",
|
||||||
|
"empty-value",
|
||||||
|
"value-with-equals",
|
||||||
|
"spaces-around",
|
||||||
|
"ignore-empty-segments",
|
||||||
|
"duplicate-last-wins",
|
||||||
|
"quoted-value",
|
||||||
|
"url-encoded",
|
||||||
|
"empty-name",
|
||||||
|
"lead-trail-with-empties",
|
||||||
|
"spaces-around-multi",
|
||||||
|
"mixed-case-from-original",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cookie_parsing_tolerates_malformed_segments(cookie_header, expected):
|
||||||
|
app, environ, start_response = setup()
|
||||||
|
|
||||||
|
from spiderweb.response import JsonResponse
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index(request):
|
||||||
|
return JsonResponse(data=request.COOKIES)
|
||||||
|
|
||||||
|
environ["HTTP_COOKIE"] = cookie_header
|
||||||
|
|
||||||
|
body = app(environ, start_response)[0].decode("utf-8")
|
||||||
|
data = _json.loads(body)
|
||||||
|
assert data == expected
|
||||||
|
|
|
@ -2,7 +2,6 @@ from io import BytesIO, BufferedReader
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from peewee import SqliteDatabase
|
|
||||||
|
|
||||||
from spiderweb import SpiderwebRouter, HttpResponse, StartupErrors, ConfigError
|
from spiderweb import SpiderwebRouter, HttpResponse, StartupErrors, ConfigError
|
||||||
from spiderweb.constants import DEFAULT_ENCODING
|
from spiderweb.constants import DEFAULT_ENCODING
|
||||||
|
@ -24,6 +23,11 @@ from spiderweb.tests.views_for_tests import (
|
||||||
form_view_without_csrf,
|
form_view_without_csrf,
|
||||||
text_view,
|
text_view,
|
||||||
unauthorized_view,
|
unauthorized_view,
|
||||||
|
file_view,
|
||||||
|
)
|
||||||
|
from spiderweb.middleware.gzip import (
|
||||||
|
CheckValidGzipMinimumLength,
|
||||||
|
CheckValidGzipCompressionLevel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,7 +51,11 @@ def test_session_middleware():
|
||||||
|
|
||||||
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
|
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
|
||||||
|
|
||||||
session_key = Session.select().first().session_key
|
_s = app.get_db_session()
|
||||||
|
try:
|
||||||
|
session_key = _s.query(Session).order_by(Session.id.asc()).first().session_key
|
||||||
|
finally:
|
||||||
|
_s.close()
|
||||||
environ["HTTP_COOKIE"] = f"swsession={session_key}"
|
environ["HTTP_COOKIE"] = f"swsession={session_key}"
|
||||||
|
|
||||||
assert app(environ, start_response) == [bytes(str(1), DEFAULT_ENCODING)]
|
assert app(environ, start_response) == [bytes(str(1), DEFAULT_ENCODING)]
|
||||||
|
@ -66,17 +74,25 @@ def test_expired_session():
|
||||||
|
|
||||||
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
|
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
|
||||||
|
|
||||||
session = Session.select().first()
|
_s = app.get_db_session()
|
||||||
session.created_at = session.created_at - timedelta(seconds=app.session_max_age)
|
try:
|
||||||
session.save()
|
session = _s.query(Session).order_by(Session.id.asc()).first()
|
||||||
|
session.created_at = session.created_at - timedelta(seconds=app.session_max_age)
|
||||||
environ["HTTP_COOKIE"] = f"swsession={session.session_key}"
|
_s.add(session)
|
||||||
|
_s.commit()
|
||||||
|
environ["HTTP_COOKIE"] = f"swsession={session.session_key}"
|
||||||
|
finally:
|
||||||
|
_s.close()
|
||||||
|
|
||||||
# it shouldn't increment because we get a new session
|
# it shouldn't increment because we get a new session
|
||||||
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
|
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
|
||||||
|
|
||||||
session2 = list(Session.select())[-1]
|
_s2 = app.get_db_session()
|
||||||
assert session2.session_key != session.session_key
|
try:
|
||||||
|
session2 = _s2.query(Session).order_by(Session.id.desc()).first()
|
||||||
|
assert session2.session_key != session.session_key
|
||||||
|
finally:
|
||||||
|
_s2.close()
|
||||||
|
|
||||||
|
|
||||||
def test_exploding_middleware():
|
def test_exploding_middleware():
|
||||||
|
@ -105,7 +121,7 @@ def test_csrf_middleware_without_session_middleware():
|
||||||
with pytest.raises(StartupErrors) as e:
|
with pytest.raises(StartupErrors) as e:
|
||||||
SpiderwebRouter(
|
SpiderwebRouter(
|
||||||
middleware=["spiderweb.middleware.csrf.CSRFMiddleware"],
|
middleware=["spiderweb.middleware.csrf.CSRFMiddleware"],
|
||||||
db=SqliteDatabase("spiderweb-tests.db"),
|
db="spiderweb-tests.db",
|
||||||
)
|
)
|
||||||
exceptiongroup = e.value.args[1]
|
exceptiongroup = e.value.args[1]
|
||||||
assert (
|
assert (
|
||||||
|
@ -152,9 +168,12 @@ def test_csrf_middleware():
|
||||||
|
|
||||||
formdata = f"name=bob&csrf_token={token}"
|
formdata = f"name=bob&csrf_token={token}"
|
||||||
environ["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
environ["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
||||||
environ["HTTP_COOKIE"] = (
|
_s = app.get_db_session()
|
||||||
f"swsession={[i for i in Session.select().dicts()][-1]['session_key']}"
|
try:
|
||||||
)
|
_last = _s.query(Session).order_by(Session.id.desc()).first()
|
||||||
|
environ["HTTP_COOKIE"] = f"swsession={_last.session_key}"
|
||||||
|
finally:
|
||||||
|
_s.close()
|
||||||
environ["REQUEST_METHOD"] = "POST"
|
environ["REQUEST_METHOD"] = "POST"
|
||||||
environ["HTTP_X_CSRF_TOKEN"] = token
|
environ["HTTP_X_CSRF_TOKEN"] = token
|
||||||
environ["CONTENT_LENGTH"] = len(formdata)
|
environ["CONTENT_LENGTH"] = len(formdata)
|
||||||
|
@ -212,9 +231,12 @@ def test_csrf_expired_token():
|
||||||
|
|
||||||
formdata = f"name=bob&csrf_token={token}"
|
formdata = f"name=bob&csrf_token={token}"
|
||||||
environ["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
environ["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
||||||
environ["HTTP_COOKIE"] = (
|
_s = app.get_db_session()
|
||||||
f"swsession={[i for i in Session.select().dicts()][-1]['session_key']}"
|
try:
|
||||||
)
|
_last = _s.query(Session).order_by(Session.id.desc()).first()
|
||||||
|
environ["HTTP_COOKIE"] = f"swsession={_last.session_key}"
|
||||||
|
finally:
|
||||||
|
_s.close()
|
||||||
environ["REQUEST_METHOD"] = "POST"
|
environ["REQUEST_METHOD"] = "POST"
|
||||||
environ["HTTP_ORIGIN"] = "example.com"
|
environ["HTTP_ORIGIN"] = "example.com"
|
||||||
environ["HTTP_X_CSRF_TOKEN"] = token
|
environ["HTTP_X_CSRF_TOKEN"] = token
|
||||||
|
@ -298,6 +320,149 @@ def test_csrf_trusted_origins():
|
||||||
assert resp2 == '{"name": "bob"}'
|
assert resp2 == '{"name": "bob"}'
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_process_middleware():
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
middleware=[
|
||||||
|
"spiderweb.tests.middleware.PostProcessingMiddleware",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_route("/", text_view)
|
||||||
|
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
|
||||||
|
assert app(environ, start_response) == [bytes("Hi! Moo!", DEFAULT_ENCODING)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_process_header_manip():
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
middleware=[
|
||||||
|
"spiderweb.tests.middleware.PostProcessingWithHeaderManipulation",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_route("/", text_view)
|
||||||
|
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
|
||||||
|
assert app(environ, start_response) == [bytes("Hi!", DEFAULT_ENCODING)]
|
||||||
|
assert start_response.get_headers()["x-moo"] == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unused_post_process_middleware():
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
middleware=[
|
||||||
|
"spiderweb.tests.middleware.ExplodingPostProcessingMiddleware",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_route("/", text_view)
|
||||||
|
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
|
||||||
|
assert app(environ, start_response) == [bytes("Hi!", DEFAULT_ENCODING)]
|
||||||
|
# make sure it kicked out the middleware and isn't just ignoring it
|
||||||
|
assert len(app.middleware) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestGzipMiddleware:
|
||||||
|
middleware = {"middleware": ["spiderweb.middleware.gzip.GzipMiddleware"]}
|
||||||
|
|
||||||
|
def test_not_enabled_on_small_response(self):
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
**self.middleware,
|
||||||
|
gzip_minimum_response_length=500,
|
||||||
|
)
|
||||||
|
app.add_route("/", text_view)
|
||||||
|
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
|
||||||
|
assert app(environ, start_response) == [bytes("Hi!", DEFAULT_ENCODING)]
|
||||||
|
assert "Content-Encoding" not in start_response.get_headers()
|
||||||
|
|
||||||
|
def test_changing_minimum_response_length(self):
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
**self.middleware,
|
||||||
|
gzip_minimum_response_length=1,
|
||||||
|
)
|
||||||
|
app.add_route("/", text_view)
|
||||||
|
|
||||||
|
environ["HTTP_ACCEPT_ENCODING"] = "gzip"
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
assert str(app(environ, start_response)[0]).startswith("b'\\x1f\\x8b\\x08")
|
||||||
|
assert "content-encoding" in start_response.get_headers()
|
||||||
|
|
||||||
|
def test_not_enabled_on_error_response(self):
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
**self.middleware,
|
||||||
|
gzip_minimum_response_length=1,
|
||||||
|
)
|
||||||
|
app.add_route("/", unauthorized_view)
|
||||||
|
|
||||||
|
environ["HTTP_ACCEPT_ENCODING"] = "gzip"
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
assert app(environ, start_response) == [bytes("Unauthorized", DEFAULT_ENCODING)]
|
||||||
|
assert "content-encoding" not in start_response.get_headers()
|
||||||
|
|
||||||
|
def test_not_enabled_on_bytes_response(self):
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
**self.middleware,
|
||||||
|
gzip_minimum_response_length=1,
|
||||||
|
)
|
||||||
|
# send a file that's already in bytes form
|
||||||
|
app.add_route("/", file_view)
|
||||||
|
|
||||||
|
environ["HTTP_ACCEPT_ENCODING"] = "gzip"
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
assert app(environ, start_response) == [bytes("hi", DEFAULT_ENCODING)]
|
||||||
|
assert "content-encoding" not in start_response.get_headers()
|
||||||
|
|
||||||
|
def test_invalid_response_length(self):
|
||||||
|
class FakeServer:
|
||||||
|
gzip_minimum_response_length = "asdf"
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError) as e:
|
||||||
|
CheckValidGzipMinimumLength(server=FakeServer).check()
|
||||||
|
assert (
|
||||||
|
e.value.args[0] == CheckValidGzipMinimumLength.INVALID_GZIP_MINIMUM_LENGTH
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_negative_response_length(self):
|
||||||
|
class FakeServer:
|
||||||
|
gzip_minimum_response_length = -1
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError) as e:
|
||||||
|
CheckValidGzipMinimumLength(server=FakeServer).check()
|
||||||
|
assert (
|
||||||
|
e.value.args[0] == CheckValidGzipMinimumLength.INVALID_GZIP_MINIMUM_LENGTH
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_bad_compression_level(self):
|
||||||
|
class FakeServer:
|
||||||
|
gzip_compression_level = "asdf"
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError) as e:
|
||||||
|
CheckValidGzipCompressionLevel(server=FakeServer).check()
|
||||||
|
assert (
|
||||||
|
e.value.args[0]
|
||||||
|
== CheckValidGzipCompressionLevel.INVALID_GZIP_COMPRESSION_LEVEL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCorsMiddleware:
|
class TestCorsMiddleware:
|
||||||
# adapted from:
|
# adapted from:
|
||||||
# https://github.com/adamchainz/django-cors-headers/blob/main/tests/test_middleware.py
|
# https://github.com/adamchainz/django-cors-headers/blob/main/tests/test_middleware.py
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from spiderweb import HttpResponse
|
from spiderweb import HttpResponse
|
||||||
from spiderweb.decorators import csrf_exempt
|
from spiderweb.decorators import csrf_exempt
|
||||||
from spiderweb.response import JsonResponse, TemplateResponse
|
from spiderweb.response import JsonResponse, TemplateResponse, FileResponse
|
||||||
|
|
||||||
|
|
||||||
EXAMPLE_HTML_FORM = """
|
EXAMPLE_HTML_FORM = """
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
|
@ -47,3 +46,7 @@ def text_view(request):
|
||||||
|
|
||||||
def unauthorized_view(request):
|
def unauthorized_view(request):
|
||||||
return HttpResponse("Unauthorized", status_code=401)
|
return HttpResponse("Unauthorized", status_code=401)
|
||||||
|
|
||||||
|
|
||||||
|
def file_view(request):
|
||||||
|
return FileResponse("spiderweb/tests/staticfiles/file_for_testing_fileresponse.txt")
|
||||||
|
|
Loading…
Add table
Reference in a new issue