Skip to content

Commit 328bf15

Browse files
authored
Drop support for Python 3.9 and Support for Python 3.14 (#233)
* Use uv * Drop support for Python 3.9 * Support for Python 3.14 * fix uv sync --upgrade * fix uv
1 parent 2d77c7d commit 328bf15

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1348
-941
lines changed

.github/workflows/docs.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ jobs:
1313

1414
- uses: astral-sh/setup-uv@v6
1515
with:
16-
python-version: "3.x"
1716
enable-cache: true
1817

1918
- name: Install dependencies
20-
run: uv sync --group doc --all-extras
19+
run: uv sync --upgrade --group doc --all-extras
2120

2221
- name: Build docs
2322
run: |

.github/workflows/publish.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ jobs:
1313

1414
- uses: astral-sh/setup-uv@v6
1515
with:
16-
python-version: "3.x"
1716
enable-cache: true
1817

1918
- name: Install dependencies
20-
run: uv sync --group build --all-extras
19+
run: uv sync --upgrade --group build --all-extras
2120

2221
- name: Build and publish
2322
env:

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
strategy:
2323
max-parallel: 5
2424
matrix:
25-
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
25+
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
2626
flask-version: [ "Flask>=2.0,<3.0", "Flask>=3.0" ]
2727
env:
2828
PYTHONPATH: .
@@ -37,7 +37,7 @@ jobs:
3737

3838
- name: Install dependencies
3939
run: |
40-
uv sync --all-extras --all-groups
40+
uv sync --upgrade --all-extras --all-groups
4141
uv add "${{ matrix.flask-version }}"
4242
4343
- name: Test with pytest

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Before submitting pr, you need to complete the following steps:
1010
1. Install requirements
1111

1212
```bash
13-
uv sync --all-extras --all-groups
13+
uv sync --upgrade --all-extras --all-groups
1414
```
1515

1616
2. Running the tests

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The key features are:
3737

3838
## Requirements
3939

40-
Python 3.9+
40+
Python 3.10+
4141

4242
flask-openapi3 is dependent on the following libraries:
4343

@@ -131,14 +131,12 @@ if __name__ == "__main__":
131131
<summary>Class-based API View Example</summary>
132132

133133
```python
134-
from typing import Optional
135-
136134
from pydantic import BaseModel, Field
137135

138136
from flask_openapi3 import OpenAPI, Tag, Info, APIView
139137

140138

141-
info = Info(title='book API', version='1.0.0')
139+
info = Info(title="book API", version="1.0.0")
142140
app = OpenAPI(__name__, info=info)
143141

144142
api_view = APIView(url_prefix="/api/v1", view_tags=[Tag(name="book")])
@@ -149,12 +147,12 @@ class BookPath(BaseModel):
149147

150148

151149
class BookQuery(BaseModel):
152-
age: Optional[int] = Field(None, description='Age')
150+
age: int | None = Field(None, description="Age")
153151

154152

155153
class BookBody(BaseModel):
156-
age: Optional[int] = Field(..., ge=2, le=4, description='Age')
157-
author: str = Field(None, min_length=2, max_length=4, description='Author')
154+
age: int | None = Field(..., ge=2, le=4, description="Age")
155+
author: str = Field(None, min_length=2, max_length=4, description="Author")
158156

159157

160158
@api_view.route("/book")

docs/Example.md

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ from pydantic import BaseModel
66
from flask_openapi3 import Info, Tag
77
from flask_openapi3 import OpenAPI
88

9-
info = Info(title='book API', version='1.0.0')
9+
info = Info(title="book API", version="1.0.0")
1010
app = OpenAPI(__name__, info=info)
1111

12-
book_tag = Tag(name='book', description='Some Book')
12+
book_tag = Tag(name="book", description="Some Book")
1313

1414

1515
class BookQuery(BaseModel):
1616
age: int
1717
author: str
1818

1919

20-
@app.get('/book', tags=[book_tag])
20+
@app.get("/book", tags=[book_tag])
2121
def get_book(query: BookQuery):
2222
"""get books
2323
to get all books
@@ -32,23 +32,21 @@ def get_book(query: BookQuery):
3232
}
3333

3434

35-
if __name__ == '__main__':
35+
if __name__ == "__main__":
3636
app.run(debug=True)
3737
```
3838

3939
## REST Demo
4040

4141
```python
4242
from http import HTTPStatus
43-
from typing import Optional, List
44-
4543
from pydantic import BaseModel, Field
4644

4745
from flask_openapi3 import Info, Tag
4846
from flask_openapi3 import OpenAPI
4947

5048

51-
info = Info(title='book API', version='1.0.0')
49+
info = Info(title="book API", version="1.0.0")
5250
# Basic Authentication Sample
5351
basic = {
5452
"type": "http",
@@ -89,44 +87,44 @@ class NotFoundResponse(BaseModel):
8987

9088
app = OpenAPI(__name__, info=info, security_schemes=security_schemes, responses={404: NotFoundResponse})
9189

92-
book_tag = Tag(name='book', description='Some Book')
90+
book_tag = Tag(name="book", description="Some Book")
9391
security = [
9492
{"jwt": []},
9593
{"oauth2": ["write:pets", "read:pets"]}
9694
]
9795

9896

9997
class BookPath(BaseModel):
100-
bid: int = Field(..., description='book id')
98+
bid: int = Field(..., description="book id")
10199

102100

103101
class BookQuery(BaseModel):
104-
age: Optional[int] = Field(None, description='Age')
105-
s_list: List[str] = Field(None, alias='s_list[]', description='some array')
102+
age: int | None = Field(None, description="Age")
103+
s_list: list[str] = Field(None, alias="s_list[]", description="some array")
106104

107105

108106
class BookBody(BaseModel):
109-
age: Optional[int] = Field(..., ge=2, le=4, description='Age')
110-
author: str = Field(None, min_length=2, max_length=4, description='Author')
107+
age: int | None = Field(..., ge=2, le=4, description="Age")
108+
author: str = Field(None, min_length=2, max_length=4, description="Author")
111109

112110

113111
class BookBodyWithID(BaseModel):
114-
bid: int = Field(..., description='book id')
115-
age: Optional[int] = Field(None, ge=2, le=4, description='Age')
116-
author: str = Field(None, min_length=2, max_length=4, description='Author')
112+
bid: int = Field(..., description="book id")
113+
age: int | None = Field(None, ge=2, le=4, description="Age")
114+
author: str = Field(None, min_length=2, max_length=4, description="Author")
117115

118116

119117
class BookResponse(BaseModel):
120118
code: int = Field(0, description="Status Code")
121119
message: str = Field("ok", description="Exception Information")
122-
data: Optional[BookBodyWithID]
120+
data: BookBodyWithID | None
123121

124122

125123
@app.get(
126-
'/book/<int:bid>',
124+
"/book/<int:bid>",
127125
tags=[book_tag],
128-
summary='new summary',
129-
description='new description',
126+
summary="new summary",
127+
description="new description",
130128
responses={200: BookResponse, 201: {"content": {"text/csv": {"schema": {"type": "string"}}}}},
131129
security=security
132130
)
@@ -137,11 +135,11 @@ def get_book(path: BookPath):
137135
"""
138136
if path.bid == 4:
139137
return NotFoundResponse().dict(), 404
140-
return {"code": 0, "message": "ok", "data": {"bid": path.bid, "age": 3, "author": 'no'}}
138+
return {"code": 0, "message": "ok", "data": {"bid": path.bid, "age": 3, "author": "no"}}
141139

142140

143141
# set doc_ui False disable openapi UI
144-
@app.get('/book', doc_ui=True, deprecated=True)
142+
@app.get("/book", doc_ui=True, deprecated=True)
145143
def get_books(query: BookQuery):
146144
"""get books
147145
to get all books
@@ -151,46 +149,44 @@ def get_books(query: BookQuery):
151149
"code": 0,
152150
"message": "ok",
153151
"data": [
154-
{"bid": 1, "age": query.age, "author": 'a1'},
155-
{"bid": 2, "age": query.age, "author": 'a2'}
152+
{"bid": 1, "age": query.age, "author": "a1"},
153+
{"bid": 2, "age": query.age, "author": "a2"}
156154
]
157155
}
158156

159157

160-
@app.post('/book', tags=[book_tag], responses={200: BookResponse})
158+
@app.post("/book", tags=[book_tag], responses={200: BookResponse})
161159
def create_book(body: BookBody):
162160
print(body)
163161
return {"code": 0, "message": "ok"}, HTTPStatus.OK
164162

165163

166-
@app.put('/book/<int:bid>', tags=[book_tag])
164+
@app.put("/book/<int:bid>", tags=[book_tag])
167165
def update_book(path: BookPath, body: BookBody):
168166
print(path)
169167
print(body)
170168
return {"code": 0, "message": "ok"}
171169

172170

173-
@app.delete('/book/<int:bid>', tags=[book_tag], doc_ui=False)
171+
@app.delete("/book/<int:bid>", tags=[book_tag], doc_ui=False)
174172
def delete_book(path: BookPath):
175173
print(path)
176174
return {"code": 0, "message": "ok"}
177175

178176

179-
if __name__ == '__main__':
177+
if __name__ == "__main__":
180178
app.run(debug=True)
181179
```
182180

183181
## APIBlueprint
184182

185183
```python
186-
from typing import Optional
187-
188184
from pydantic import BaseModel, Field
189185

190186
from flask_openapi3 import APIBlueprint, OpenAPI
191187
from flask_openapi3 import Tag, Info
192188

193-
info = Info(title='book API', version='1.0.0')
189+
info = Info(title="book API", version="1.0.0")
194190

195191
jwt = {
196192
"type": "http",
@@ -201,7 +197,7 @@ security_schemes = {"jwt": jwt}
201197

202198
app = OpenAPI(__name__, info=info, security_schemes=security_schemes)
203199

204-
tag = Tag(name='book', description="Some Book")
200+
tag = Tag(name="book", description="Some Book")
205201
security = [{"jwt": []}]
206202

207203

@@ -211,9 +207,9 @@ class Unauthorized(BaseModel):
211207

212208

213209
api = APIBlueprint(
214-
'/book',
210+
"/book",
215211
__name__,
216-
url_prefix='/api',
212+
url_prefix="/api",
217213
abp_tags=[tag],
218214
abp_security=security,
219215
abp_responses={"401": Unauthorized},
@@ -223,26 +219,26 @@ api = APIBlueprint(
223219

224220

225221
class BookBody(BaseModel):
226-
age: Optional[int] = Field(..., ge=2, le=4, description='Age')
227-
author: str = Field(None, min_length=2, max_length=4, description='Author')
222+
age: int | None = Field(..., ge=2, le=4, description="Age")
223+
author: str = Field(None, min_length=2, max_length=4, description="Author")
228224

229225

230226
class Path(BaseModel):
231-
bid: int = Field(..., description='book id')
227+
bid: int = Field(..., description="book id")
232228

233229

234-
@api.get('/book', doc_ui=False)
230+
@api.get("/book", doc_ui=False)
235231
def get_book():
236232
return {"code": 0, "message": "ok"}
237233

238234

239-
@api.post('/book', responses={201: {"content": {"text/csv": {"schema": {"type": "string"}}}}})
235+
@api.post("/book", responses={201: {"content": {"text/csv": {"schema": {"type": "string"}}}}})
240236
def create_book(body: BookBody):
241237
assert body.age == 3
242238
return {"code": 0, "message": "ok"}
243239

244240

245-
@api.put('/book/<int:bid>')
241+
@api.put("/book/<int:bid>")
246242
def update_book(path: Path, body: BookBody):
247243
assert path.bid == 1
248244
assert body.age == 3
@@ -252,7 +248,7 @@ def update_book(path: Path, body: BookBody):
252248
# register api
253249
app.register_api(api)
254250

255-
if __name__ == '__main__':
251+
if __name__ == "__main__":
256252
app.run(debug=True)
257253
```
258254

@@ -271,15 +267,15 @@ class UploadFileForm(BaseModel):
271267
file_type: str = Field(None, description="File Type")
272268

273269

274-
@app.post('/upload')
270+
@app.post("/upload")
275271
def upload_file(form: UploadFileForm):
276272
print(form.file.filename)
277273
print(form.file_type)
278-
form.file.save('test.jpg')
274+
form.file.save("test.jpg")
279275
return {"code": 0, "message": "ok"}
280276

281277

282-
if __name__ == '__main__':
278+
if __name__ == "__main__":
283279
app.run(debug=True)
284280
```
285281

docs/Usage/Model_Config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The `openapi_extra` will be merged with the automatically generated OpenAPI sche
1111
```python
1212
class UploadFilesForm(BaseModel):
1313
file: FileStorage
14-
str_list: List[str]
14+
str_list: list[str]
1515

1616
model_config = dict(
1717
openapi_extra={
@@ -82,7 +82,7 @@ You can use `reqiured` in `openapi_extra` to mark the RequestBody as Optional.
8282

8383
```python
8484
class PingBody(BaseModel):
85-
ping: Optional[str] = Field("ok", description="String to return, 'ok' when null.")
85+
ping: str | None = Field("ok", description="String to return, 'ok' when null.")
8686

8787
model_config = dict(
8888
openapi_extra = {

docs/Usage/Request.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ like [path](#path), you need pass **`query`** to view function.
3636

3737
```python hl_lines="7"
3838
class BookQuery(BaseModel):
39-
age: Optional[int] = Field(..., ge=2, le=4, description='Age')
39+
age: int | None = Field(..., ge=2, le=4, description='Age')
4040
author: str = Field(None, min_length=2, max_length=4, description='Author')
4141

4242

@@ -66,7 +66,7 @@ Receive flask **`request.json`**.
6666

6767
```python hl_lines="7"
6868
class BookBody(BaseModel):
69-
age: Optional[int] = Field(..., ge=2, le=4, description='Age')
69+
age: int | None = Field(..., ge=2, le=4, description='Age')
7070
author: str = Field(None, min_length=2, max_length=4, description='Author')
7171

7272

docs/Usage/Response.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ If you want to generate **Schemas**, pass the **`responses`**.
55
```python hl_lines="13"
66
class BookBodyWithID(BaseModel):
77
bid: int = Field(..., description='book id')
8-
age: Optional[int] = Field(None, ge=2, le=4, description='Age')
8+
age: int | None = Field(None, ge=2, le=4, description='Age')
99
author: str = Field(None, min_length=2, max_length=4, description='Author')
1010

1111

0 commit comments

Comments
 (0)