fastapi-fsp 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/PKG-INFO +15 -5
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/README.md +14 -4
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/fastapi_fsp/fsp.py +82 -13
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/pyproject.toml +1 -1
- fastapi_fsp-0.1.2/tests/test_fsp_filters_indexed_sync.py +46 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/uv.lock +8 -2
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/.github/workflows/ci.yml +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/.github/workflows/release.yml +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/.gitignore +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/.pre-commit-config.yaml +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/LICENSE +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/PROJECT.md +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/fastapi_fsp/__init__.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/fastapi_fsp/models.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/main.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/pytest.ini +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/__init__.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/conftest.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/conftest_async.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/main.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/main_async.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/test_fsp.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/test_fsp_async.py +0 -0
- {fastapi_fsp-0.1.1 → fastapi_fsp-0.1.2}/tests/test_fsp_filters_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-fsp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel
|
|
5
5
|
Project-URL: Homepage, https://github.com/fromej-dev/fastapi-fsp
|
|
6
6
|
Project-URL: Repository, https://github.com/fromej-dev/fastapi-fsp
|
|
@@ -109,7 +109,9 @@ Sorting:
|
|
|
109
109
|
- sort_by: the field name, e.g., `name`
|
|
110
110
|
- order: `asc` or `desc`
|
|
111
111
|
|
|
112
|
-
Filtering (
|
|
112
|
+
Filtering (two supported formats):
|
|
113
|
+
|
|
114
|
+
1) Simple (triplets repeated in the query string):
|
|
113
115
|
- field: the field/column name, e.g., `name`
|
|
114
116
|
- operator: one of
|
|
115
117
|
- eq, ne
|
|
@@ -122,17 +124,25 @@ Filtering (repeatable sets; arrays are supported by sending multiple parameters)
|
|
|
122
124
|
- contains, starts_with, ends_with (translated to LIKE patterns)
|
|
123
125
|
- value: raw string value (or list-like comma-separated depending on operator)
|
|
124
126
|
|
|
125
|
-
Examples:
|
|
127
|
+
Examples (simple format):
|
|
126
128
|
- `?field=name&operator=eq&value=Deadpond`
|
|
127
129
|
- `?field=age&operator=between&value=18,30`
|
|
128
130
|
- `?field=name&operator=in&value=Deadpond,Rusty-Man`
|
|
129
131
|
- `?field=name&operator=contains&value=man`
|
|
132
|
+
- Chain multiple filters by repeating the triplet: `?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust`
|
|
133
|
+
|
|
134
|
+
2) Indexed format (useful for clients that handle arrays of objects):
|
|
135
|
+
- Use keys like `filters[0][field]`, `filters[0][operator]`, `filters[0][value]`, then increment the index for additional filters (`filters[1][...]`, etc.).
|
|
130
136
|
|
|
131
|
-
|
|
137
|
+
Example (indexed format):
|
|
132
138
|
```
|
|
133
|
-
?field=age&operator=gte&value=18&field=name&operator=ilike&value=
|
|
139
|
+
?filters[0][field]=age&filters[0][operator]=gte&filters[0][value]=18&filters[1][field]=name&filters[1][operator]=ilike&filters[1][value]=joy
|
|
134
140
|
```
|
|
135
141
|
|
|
142
|
+
Notes:
|
|
143
|
+
- Both formats are equivalent; the indexed format takes precedence if present.
|
|
144
|
+
- If any filter is incomplete (missing operator or value in the indexed form, or mismatched counts of simple triplets), the API responds with HTTP 400.
|
|
145
|
+
|
|
136
146
|
## Response model
|
|
137
147
|
|
|
138
148
|
```
|
|
@@ -84,7 +84,9 @@ Sorting:
|
|
|
84
84
|
- sort_by: the field name, e.g., `name`
|
|
85
85
|
- order: `asc` or `desc`
|
|
86
86
|
|
|
87
|
-
Filtering (
|
|
87
|
+
Filtering (two supported formats):
|
|
88
|
+
|
|
89
|
+
1) Simple (triplets repeated in the query string):
|
|
88
90
|
- field: the field/column name, e.g., `name`
|
|
89
91
|
- operator: one of
|
|
90
92
|
- eq, ne
|
|
@@ -97,17 +99,25 @@ Filtering (repeatable sets; arrays are supported by sending multiple parameters)
|
|
|
97
99
|
- contains, starts_with, ends_with (translated to LIKE patterns)
|
|
98
100
|
- value: raw string value (or list-like comma-separated depending on operator)
|
|
99
101
|
|
|
100
|
-
Examples:
|
|
102
|
+
Examples (simple format):
|
|
101
103
|
- `?field=name&operator=eq&value=Deadpond`
|
|
102
104
|
- `?field=age&operator=between&value=18,30`
|
|
103
105
|
- `?field=name&operator=in&value=Deadpond,Rusty-Man`
|
|
104
106
|
- `?field=name&operator=contains&value=man`
|
|
107
|
+
- Chain multiple filters by repeating the triplet: `?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust`
|
|
108
|
+
|
|
109
|
+
2) Indexed format (useful for clients that handle arrays of objects):
|
|
110
|
+
- Use keys like `filters[0][field]`, `filters[0][operator]`, `filters[0][value]`, then increment the index for additional filters (`filters[1][...]`, etc.).
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
Example (indexed format):
|
|
107
113
|
```
|
|
108
|
-
?field=age&operator=gte&value=18&field=name&operator=ilike&value=
|
|
114
|
+
?filters[0][field]=age&filters[0][operator]=gte&filters[0][value]=18&filters[1][field]=name&filters[1][operator]=ilike&filters[1][value]=joy
|
|
109
115
|
```
|
|
110
116
|
|
|
117
|
+
Notes:
|
|
118
|
+
- Both formats are equivalent; the indexed format takes precedence if present.
|
|
119
|
+
- If any filter is incomplete (missing operator or value in the indexed form, or mismatched counts of simple triplets), the API responds with HTTP 400.
|
|
120
|
+
|
|
111
121
|
## Response model
|
|
112
122
|
|
|
113
123
|
```
|
|
@@ -2,6 +2,7 @@ import math
|
|
|
2
2
|
from typing import Annotated, Any, List, Optional
|
|
3
3
|
|
|
4
4
|
from fastapi import Depends, HTTPException, Query, Request, status
|
|
5
|
+
from pydantic import ValidationError
|
|
5
6
|
from sqlalchemy import Select, func
|
|
6
7
|
from sqlmodel import Session, not_, select
|
|
7
8
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
@@ -19,22 +20,90 @@ from fastapi_fsp.models import (
|
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
def _parse_one_filter_at(i: int, field: str, operator: str, value: str) -> Filter:
|
|
24
|
+
try:
|
|
25
|
+
filter_ = Filter(field=field, operator=FilterOperator(operator), value=value)
|
|
26
|
+
except ValidationError as e:
|
|
27
|
+
raise HTTPException(
|
|
28
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
29
|
+
detail=f"Invalid filter at index {i}: {str(e)}",
|
|
30
|
+
) from e
|
|
31
|
+
except ValueError as e:
|
|
32
|
+
raise HTTPException(
|
|
33
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
34
|
+
detail=f"Invalid operator '{operator}' at index {i}.",
|
|
35
|
+
) from e
|
|
36
|
+
return filter_
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_array_of_filters(
|
|
40
|
+
fields: List[str], operators: List[str], values: List[str]
|
|
41
|
+
) -> List[Filter]:
|
|
42
|
+
# Validate that we have matching lengths
|
|
43
|
+
if not (len(fields) == len(operators) == len(values)):
|
|
31
44
|
raise HTTPException(
|
|
32
45
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
33
|
-
detail="Mismatched filter parameters.",
|
|
46
|
+
detail="Mismatched filter parameters in array format.",
|
|
34
47
|
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
return [
|
|
49
|
+
_parse_one_filter_at(i, field, operator, value)
|
|
50
|
+
for i, (field, operator, value) in enumerate(zip(fields, operators, values))
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_filters(
|
|
55
|
+
request: Request,
|
|
56
|
+
) -> Optional[List[Filter]]:
|
|
57
|
+
"""
|
|
58
|
+
Parse filters from query parameters supporting two formats:
|
|
59
|
+
1. Indexed format:
|
|
60
|
+
?filters[0][field]=age&filters[0][operator]=gte&filters[0][value]=18&filters[1][field]=name&filters[1][operator]=ilike&filters[1][value]=joy
|
|
61
|
+
2. Simple format:
|
|
62
|
+
?field=age&operator=gte&value=18&field=name&operator=ilike&value=joy
|
|
63
|
+
"""
|
|
64
|
+
query_params = request.query_params
|
|
65
|
+
filters = []
|
|
66
|
+
|
|
67
|
+
# Try indexed format first: filters[0][field], filters[0][operator], etc.
|
|
68
|
+
i = 0
|
|
69
|
+
while True:
|
|
70
|
+
field_key = f"filters[{i}][field]"
|
|
71
|
+
operator_key = f"filters[{i}][operator]"
|
|
72
|
+
value_key = f"filters[{i}][value]"
|
|
73
|
+
|
|
74
|
+
field = query_params.get(field_key)
|
|
75
|
+
operator = query_params.get(operator_key)
|
|
76
|
+
value = query_params.get(value_key)
|
|
77
|
+
|
|
78
|
+
# If we don't have a field at this index, break the loop
|
|
79
|
+
if field is None:
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
# Validate that we have all required parts
|
|
83
|
+
if operator is None or value is None:
|
|
84
|
+
raise HTTPException(
|
|
85
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
86
|
+
detail=f"Incomplete filter at index {i}. Missing operator or value.",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
filters.append(_parse_one_filter_at(i, field, operator, value))
|
|
90
|
+
i += 1
|
|
91
|
+
|
|
92
|
+
# If we found indexed filters, return them
|
|
93
|
+
if filters:
|
|
94
|
+
return filters
|
|
95
|
+
|
|
96
|
+
# Fall back to simple format: field, operator, value
|
|
97
|
+
filters = _parse_array_of_filters(
|
|
98
|
+
query_params.getlist("field"),
|
|
99
|
+
query_params.getlist("operator"),
|
|
100
|
+
query_params.getlist("value"),
|
|
101
|
+
)
|
|
102
|
+
if filters:
|
|
103
|
+
return filters
|
|
104
|
+
|
|
105
|
+
# No filters found
|
|
106
|
+
return None
|
|
38
107
|
|
|
39
108
|
|
|
40
109
|
def _parse_sort(
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from fastapi.testclient import TestClient
|
|
2
|
+
from sqlmodel import Session
|
|
3
|
+
|
|
4
|
+
from tests.main import Hero
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def seed(session: Session):
|
|
8
|
+
session.add_all(
|
|
9
|
+
[
|
|
10
|
+
Hero(name="Deadpond", secret_name="Dive Wilson", age=None),
|
|
11
|
+
Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48),
|
|
12
|
+
Hero(name="ALPHA", secret_name="Alpha Secret", age=10),
|
|
13
|
+
Hero(name="beta", secret_name="Beta Secret", age=20),
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
session.commit()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_indexed_single_filter_eq(session: Session, client: TestClient):
|
|
20
|
+
seed(session)
|
|
21
|
+
r = client.get(
|
|
22
|
+
"/heroes/?filters[0][field]=name&filters[0][operator]=eq&filters[0][value]=Deadpond"
|
|
23
|
+
)
|
|
24
|
+
assert r.status_code == 200
|
|
25
|
+
js = r.json()
|
|
26
|
+
assert len(js["data"]) == 1
|
|
27
|
+
assert js["data"][0]["name"] == "Deadpond"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_indexed_multiple_filters_combined(session: Session, client: TestClient):
|
|
31
|
+
seed(session)
|
|
32
|
+
# age >= 18 AND name ILIKE '%eta'
|
|
33
|
+
r = client.get(
|
|
34
|
+
"/heroes/?filters[0][field]=age&filters[0][operator]=gte&filters[0][value]=18"
|
|
35
|
+
"&filters[1][field]=name&filters[1][operator]=ilike&filters[1][value]=%25eta"
|
|
36
|
+
)
|
|
37
|
+
assert r.status_code == 200
|
|
38
|
+
names = [h["name"] for h in r.json()["data"]]
|
|
39
|
+
# Only 'beta' is age >= 18 and ends with 'eta'
|
|
40
|
+
assert set(names) == {"beta"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_indexed_incomplete_filter_returns_400(session: Session, client: TestClient):
|
|
44
|
+
seed(session)
|
|
45
|
+
r = client.get("/heroes/?filters[0][field]=age&filters[0][operator]=gte")
|
|
46
|
+
assert r.status_code == 400
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
3
3
|
requires-python = ">=3.12"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
@@ -167,7 +167,7 @@ wheels = [
|
|
|
167
167
|
|
|
168
168
|
[[package]]
|
|
169
169
|
name = "fastapi-fsp"
|
|
170
|
-
version = "0.1.
|
|
170
|
+
version = "0.1.2"
|
|
171
171
|
source = { editable = "." }
|
|
172
172
|
dependencies = [
|
|
173
173
|
{ name = "fastapi" },
|
|
@@ -229,6 +229,8 @@ wheels = [
|
|
|
229
229
|
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
|
230
230
|
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
|
231
231
|
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
|
|
232
|
+
{ url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
|
|
233
|
+
{ url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
|
|
232
234
|
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
|
|
233
235
|
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
|
234
236
|
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
|
@@ -238,6 +240,8 @@ wheels = [
|
|
|
238
240
|
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
|
239
241
|
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
|
240
242
|
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
|
243
|
+
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
|
244
|
+
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
|
241
245
|
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
|
242
246
|
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
|
243
247
|
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
|
@@ -245,6 +249,8 @@ wheels = [
|
|
|
245
249
|
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
|
246
250
|
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
|
247
251
|
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
|
252
|
+
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
|
253
|
+
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
|
|
248
254
|
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
|
249
255
|
]
|
|
250
256
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|