APIFlask 2.2.1__tar.gz → 2.3.0__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.
Files changed (92) hide show
  1. {apiflask-2.2.1 → apiflask-2.3.0}/CHANGES.md +9 -0
  2. {apiflask-2.2.1 → apiflask-2.3.0}/PKG-INFO +1 -1
  3. {apiflask-2.2.1 → apiflask-2.3.0}/pyproject.toml +1 -1
  4. {apiflask-2.2.1 → apiflask-2.3.0}/requirements/examples.txt +1 -1
  5. {apiflask-2.2.1 → apiflask-2.3.0}/requirements/min-versions.txt +1 -1
  6. {apiflask-2.2.1 → apiflask-2.3.0}/src/APIFlask.egg-info/PKG-INFO +1 -1
  7. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/scaffold.py +58 -0
  8. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_decorator_input.py +136 -0
  9. {apiflask-2.2.1 → apiflask-2.3.0}/tox.ini +1 -1
  10. {apiflask-2.2.1 → apiflask-2.3.0}/LICENSE +0 -0
  11. {apiflask-2.2.1 → apiflask-2.3.0}/MANIFEST.in +0 -0
  12. {apiflask-2.2.1 → apiflask-2.3.0}/NOTICE +0 -0
  13. {apiflask-2.2.1 → apiflask-2.3.0}/README.md +0 -0
  14. {apiflask-2.2.1 → apiflask-2.3.0}/examples/README.md +0 -0
  15. {apiflask-2.2.1 → apiflask-2.3.0}/examples/auth/basic_auth/app.py +0 -0
  16. {apiflask-2.2.1 → apiflask-2.3.0}/examples/auth/token_auth/app.py +0 -0
  17. {apiflask-2.2.1 → apiflask-2.3.0}/examples/base_response/app.py +0 -0
  18. {apiflask-2.2.1 → apiflask-2.3.0}/examples/basic/app.py +0 -0
  19. {apiflask-2.2.1 → apiflask-2.3.0}/examples/blueprint_tags/app.py +0 -0
  20. {apiflask-2.2.1 → apiflask-2.3.0}/examples/cbv/app.py +0 -0
  21. {apiflask-2.2.1 → apiflask-2.3.0}/examples/dataclass/app.py +0 -0
  22. {apiflask-2.2.1 → apiflask-2.3.0}/examples/file_upload/app.py +0 -0
  23. {apiflask-2.2.1 → apiflask-2.3.0}/examples/file_upload/upload/.gitkeep +0 -0
  24. {apiflask-2.2.1 → apiflask-2.3.0}/examples/openapi/basic/app.py +0 -0
  25. {apiflask-2.2.1 → apiflask-2.3.0}/examples/openapi/custom_decorators/app.py +0 -0
  26. {apiflask-2.2.1 → apiflask-2.3.0}/examples/openapi/static_docs/app.py +0 -0
  27. {apiflask-2.2.1 → apiflask-2.3.0}/examples/openapi/static_docs/generate_docs.sh +0 -0
  28. {apiflask-2.2.1 → apiflask-2.3.0}/examples/openapi/static_docs/index.html +0 -0
  29. {apiflask-2.2.1 → apiflask-2.3.0}/examples/openapi/static_docs/openapi.json +0 -0
  30. {apiflask-2.2.1 → apiflask-2.3.0}/examples/orm/app.py +0 -0
  31. {apiflask-2.2.1 → apiflask-2.3.0}/examples/pagination/app.py +0 -0
  32. {apiflask-2.2.1 → apiflask-2.3.0}/examples/requirements.txt +0 -0
  33. {apiflask-2.2.1 → apiflask-2.3.0}/examples/test_examples.py +0 -0
  34. {apiflask-2.2.1 → apiflask-2.3.0}/requirements/dev.txt +0 -0
  35. {apiflask-2.2.1 → apiflask-2.3.0}/requirements/docs.txt +0 -0
  36. {apiflask-2.2.1 → apiflask-2.3.0}/requirements/tests.txt +0 -0
  37. {apiflask-2.2.1 → apiflask-2.3.0}/requirements/typing.txt +0 -0
  38. {apiflask-2.2.1 → apiflask-2.3.0}/setup.cfg +0 -0
  39. {apiflask-2.2.1 → apiflask-2.3.0}/src/APIFlask.egg-info/SOURCES.txt +0 -0
  40. {apiflask-2.2.1 → apiflask-2.3.0}/src/APIFlask.egg-info/dependency_links.txt +0 -0
  41. {apiflask-2.2.1 → apiflask-2.3.0}/src/APIFlask.egg-info/entry_points.txt +0 -0
  42. {apiflask-2.2.1 → apiflask-2.3.0}/src/APIFlask.egg-info/requires.txt +0 -0
  43. {apiflask-2.2.1 → apiflask-2.3.0}/src/APIFlask.egg-info/top_level.txt +0 -0
  44. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/__init__.py +0 -0
  45. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/app.py +0 -0
  46. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/blueprint.py +0 -0
  47. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/commands.py +0 -0
  48. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/exceptions.py +0 -0
  49. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/fields.py +0 -0
  50. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/helpers.py +0 -0
  51. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/openapi.py +0 -0
  52. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/py.typed +0 -0
  53. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/route.py +0 -0
  54. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/schemas.py +0 -0
  55. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/security.py +0 -0
  56. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/settings.py +0 -0
  57. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/types.py +0 -0
  58. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/ui_templates.py +0 -0
  59. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/validators.py +0 -0
  60. {apiflask-2.2.1 → apiflask-2.3.0}/src/apiflask/views.py +0 -0
  61. {apiflask-2.2.1 → apiflask-2.3.0}/tests/__init__.py +0 -0
  62. {apiflask-2.2.1 → apiflask-2.3.0}/tests/conftest.py +0 -0
  63. {apiflask-2.2.1 → apiflask-2.3.0}/tests/schemas.py +0 -0
  64. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_app.py +0 -0
  65. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_apps/__init__.py +0 -0
  66. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_async.py +0 -0
  67. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_base_response.py +0 -0
  68. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_blueprint.py +0 -0
  69. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_commands.py +0 -0
  70. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_decorator_auth_required.py +0 -0
  71. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_decorator_doc.py +0 -0
  72. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_decorator_output.py +0 -0
  73. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_decorators.py +0 -0
  74. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_exceptions.py +0 -0
  75. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_fields.py +0 -0
  76. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_helpers.py +0 -0
  77. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_basic.py +0 -0
  78. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_blueprint.py +0 -0
  79. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_extensions.py +0 -0
  80. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_headers.py +0 -0
  81. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_info.py +0 -0
  82. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_paths.py +0 -0
  83. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_security.py +0 -0
  84. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_openapi_tags.py +0 -0
  85. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_route.py +0 -0
  86. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_schemas.py +0 -0
  87. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_security.py +0 -0
  88. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_settings_api_docs.py +0 -0
  89. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_settings_auto_behaviour.py +0 -0
  90. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_settings_openapi_fields.py +0 -0
  91. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_settings_openapi_spec.py +0 -0
  92. {apiflask-2.2.1 → apiflask-2.3.0}/tests/test_settings_response_customization.py +0 -0
@@ -1,3 +1,12 @@
1
+ ## Version 2.3.0
2
+
3
+ Released: 2024/11/25
4
+
5
+ - Support skipping the validation for the request body with `@input(validation=False)` ([issue #573][issue_573]).
6
+ - Enable CI test for Python 3.13.
7
+
8
+ [issue_573]: https://github.com/apiflask/apiflask/issues/573
9
+
1
10
 
2
11
  ## Version 2.2.1
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: APIFlask
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: A lightweight web API framework based on Flask and marshmallow-code projects.
5
5
  Author-email: Grey Li <withlihui@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ description = "A lightweight web API framework based on Flask and marshmallow-co
4
4
  readme = "README.md"
5
5
  license = { text = "MIT" }
6
6
  authors = [{ name = "Grey Li", email = "withlihui@gmail.com" }]
7
- version = "2.2.1"
7
+ version = "2.3.0"
8
8
  classifiers = [
9
9
  "Development Status :: 3 - Alpha",
10
10
  "Environment :: Web Environment",
@@ -19,7 +19,7 @@ flask==3.0.3
19
19
  # via flask-sqlalchemy
20
20
  flask-sqlalchemy==3.1.1
21
21
  # via -r requirements/examples.in
22
- greenlet==3.0.3
22
+ greenlet==3.1.0
23
23
  # via sqlalchemy
24
24
  itsdangerous==2.2.0
25
25
  # via flask
@@ -27,7 +27,7 @@ flask-marshmallow==1.0.0
27
27
  # via -r requirements/min-versions.in
28
28
  flask-sqlalchemy==2.5.1
29
29
  # via -r requirements/min-versions.in
30
- greenlet==3.0.3
30
+ greenlet==3.1.0
31
31
  # via sqlalchemy
32
32
  itsdangerous==2.2.0
33
33
  # via flask
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: APIFlask
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: A lightweight web API framework based on Flask and marshmallow-code projects.
5
5
  Author-email: Grey Li <withlihui@gmail.com>
6
6
  License: MIT
@@ -6,6 +6,7 @@ from functools import wraps
6
6
 
7
7
  from flask import current_app
8
8
  from flask import jsonify
9
+ from flask import request
9
10
  from flask import Response
10
11
  from marshmallow import ValidationError as MarshmallowValidationError
11
12
  from webargs.flaskparser import FlaskParser as BaseFlaskParser
@@ -100,6 +101,46 @@ def _generate_schema_from_mapping(schema: DictSchemaType, schema_name: str | Non
100
101
  return Schema.from_dict(schema, name=schema_name)() # type: ignore
101
102
 
102
103
 
104
+ def _load_raw_data(location: str = 'json') -> dict[t.Any, t.Any]:
105
+ """
106
+ Load raw data from the specified location in the request.
107
+
108
+ Arguments:
109
+ location (str): Specifies the source of the data to load, which can be one of the following:
110
+ - `json`: Load data from the request's JSON body.
111
+ - `form`: Load data from the request's form.
112
+ - `files` or `form_and_files`: Load data from both the request's form and files.
113
+ - `json_or_form`: Preferably load data from the request's JSON body or request's form.
114
+ - `query`: Load data from the request's query parameters.
115
+ - `path`: Load data from the request's view arguments.
116
+
117
+ `headers` and `cookies` are not supported by this function.
118
+
119
+ Returns:
120
+ dict: A dictionary containing the data loaded from the specified location.
121
+
122
+ Raises:
123
+ ValueError: If the provided location argument is not one of the expected values.
124
+ """
125
+ if location == 'json':
126
+ if request.is_json:
127
+ return request.get_json() or {}
128
+ return {}
129
+ if location == 'form':
130
+ return request.form.to_dict()
131
+ if location in ['files', 'form_and_files']:
132
+ return {**request.files.to_dict(), **request.form.to_dict()}
133
+ if location == 'json_or_form':
134
+ if request.is_json:
135
+ return request.get_json() or {}
136
+ return request.form.to_dict()
137
+ if location in ['query', 'querystring']:
138
+ return request.args.to_dict()
139
+ if location == 'path':
140
+ return request.view_args or {}
141
+ raise ValueError(f'Invalid location argument: {location}')
142
+
143
+
103
144
  class APIScaffold:
104
145
  """A base class for [`APIFlask`][apiflask.app.APIFlask] and
105
146
  [`APIBlueprint`][apiflask.blueprint.APIBlueprint].
@@ -209,6 +250,7 @@ class APIScaffold:
209
250
  schema_name: str | None = None,
210
251
  example: t.Any | None = None,
211
252
  examples: dict[str, t.Any] | None = None,
253
+ validation: bool = True,
212
254
  **kwargs: t.Any,
213
255
  ) -> t.Callable[[DecoratedType], DecoratedType]:
214
256
  """Add input settings for view functions.
@@ -262,6 +304,11 @@ class APIScaffold:
262
304
  },
263
305
  }
264
306
  ```
307
+ validation: Flag to allow disabling of validation on input. Default to `True`.
308
+
309
+ *Version changed: 2.2.2
310
+
311
+ - Add parameter `validation` to allow disabling of validation on input.
265
312
 
266
313
  *Version changed: 2.0.0*
267
314
 
@@ -290,6 +337,17 @@ class APIScaffold:
290
337
 
291
338
  def decorator(f):
292
339
  f = _ensure_sync(f)
340
+
341
+ if not validation:
342
+
343
+ @wraps(f)
344
+ def wrapper(*args: t.Any, **kwargs: t.Any):
345
+ raw_data = _load_raw_data(location)
346
+ kwargs[f'{location}_data'] = raw_data
347
+ return f(*args, **kwargs)
348
+
349
+ return wrapper
350
+
293
351
  is_body_location = location in BODY_LOCATIONS
294
352
  if is_body_location and hasattr(f, '_spec') and 'body' in f._spec:
295
353
  raise RuntimeError(
@@ -11,7 +11,10 @@ from .schemas import Files
11
11
  from .schemas import Foo
12
12
  from .schemas import Form
13
13
  from .schemas import FormAndFiles
14
+ from apiflask import Schema
14
15
  from apiflask.fields import String
16
+ from apiflask.validators import Length
17
+ from apiflask.validators import OneOf
15
18
 
16
19
 
17
20
  def test_input(app, client):
@@ -413,3 +416,136 @@ def test_input_body_example(app, client):
413
416
  rv.json['paths']['/baz']['post']['requestBody']['content']['application/json']['examples']
414
417
  == examples
415
418
  )
419
+
420
+
421
+ def test_skip_validation(app, client):
422
+ incorrect_json = {'name': 'Kitty', 'category': 'unknown'}
423
+
424
+ class PetIn(Schema):
425
+ name = String(required=True, validate=Length(0, 10))
426
+ category = String(required=True, validate=OneOf(['dog', 'cat']))
427
+
428
+ @app.patch('/pets_without_validation/<int:pet_id>')
429
+ @app.input(PetIn, validation=False)
430
+ def pets_without_validation(pet_id, json_data):
431
+ return {'pet_id': pet_id, 'json_data': json_data}
432
+
433
+ no_validated_rv = client.patch('/pets_without_validation/1', json=incorrect_json)
434
+ assert no_validated_rv.status_code == 200
435
+ assert no_validated_rv.json['json_data']['name'] == 'Kitty'
436
+ assert no_validated_rv.json['json_data']['category'] == 'unknown'
437
+
438
+
439
+ def test_location_raw_data(app):
440
+ from apiflask.scaffold import _load_raw_data
441
+
442
+ # will raises ValueError
443
+ location = 'unknown'
444
+ with pytest.raises(ValueError, match=f'Invalid location argument: {location}'):
445
+ _load_raw_data('unknown')
446
+
447
+ # `json` location
448
+ with app.test_request_context('/load_raw_data', method='POST', json={'name': 'Kitty'}):
449
+ json_data = _load_raw_data()
450
+ assert json_data['name'] == 'Kitty'
451
+
452
+ with app.test_request_context('/load_raw_data', method='POST', data={'name': 'Kitty'}):
453
+ json_data = _load_raw_data()
454
+ assert json_data == {}
455
+
456
+ # `form` location
457
+ with app.test_request_context(
458
+ '/load_raw_data',
459
+ method='POST',
460
+ data={'username': 'Kitty', 'password': '<PASSWORD>'},
461
+ content_type='application/x-www-form-urlencoded',
462
+ ):
463
+ form_data = _load_raw_data('form')
464
+ assert form_data['username'] == 'Kitty'
465
+ assert form_data['password'] == '<PASSWORD>'
466
+
467
+ with app.test_request_context(
468
+ '/load_raw_data',
469
+ method='POST',
470
+ json={'username': 'Kitty', 'password': '<PASSWORD>'},
471
+ ):
472
+ form_data = _load_raw_data('form')
473
+ assert form_data == {}
474
+
475
+ # `files` or `form_and_files` location
476
+ with app.test_request_context(
477
+ '/load_raw_data',
478
+ method='POST',
479
+ data={'file': (io.BytesIO(b'Hello World!'), 'demo.txt')},
480
+ content_type='multipart/form-data',
481
+ ):
482
+ files_data = _load_raw_data('files')
483
+ assert files_data['file'].filename == 'demo.txt'
484
+ assert files_data['file'].stream.read() == b'Hello World!'
485
+
486
+ with app.test_request_context(
487
+ '/load_raw_data',
488
+ method='POST',
489
+ data={'name': 'Kitty', 'file': (io.BytesIO(b'Hello World2!'), 'demo2.txt')},
490
+ content_type='multipart/form-data',
491
+ ):
492
+ files_and_form_rv = _load_raw_data('form_and_files')
493
+ assert files_and_form_rv['file'].filename == 'demo2.txt'
494
+ assert files_and_form_rv['file'].stream.read() == b'Hello World2!'
495
+ assert files_and_form_rv['name'] == 'Kitty'
496
+
497
+ with app.test_request_context(
498
+ '/load_raw_data',
499
+ method='POST',
500
+ json={'username': 'Kitty', 'password': '<PASSWORD>'},
501
+ ):
502
+ form_and_files_data = _load_raw_data('form_and_files')
503
+ assert form_and_files_data == {}
504
+
505
+ # `query` location
506
+ with app.test_request_context(
507
+ '/load_raw_data?name=Kitty',
508
+ method='GET',
509
+ ):
510
+ query_data = _load_raw_data('query')
511
+ assert query_data == {'name': 'Kitty'}
512
+
513
+ with app.test_request_context(
514
+ '/load_raw_data',
515
+ method='POST',
516
+ ):
517
+ query_data = _load_raw_data('query')
518
+ assert query_data == {}
519
+
520
+ # `json_or_form`
521
+ with app.test_request_context(
522
+ '/load_raw_data',
523
+ data={'username': 'Kitty', 'password': '<PASSWORD>'},
524
+ content_type='application/x-www-form-urlencoded',
525
+ ):
526
+ json_or_form_rv = _load_raw_data('json_or_form')
527
+ assert json_or_form_rv['username'] == 'Kitty'
528
+ assert json_or_form_rv['password'] == '<PASSWORD>'
529
+
530
+ with app.test_request_context(
531
+ '/load_raw_data',
532
+ json={'currentId': '<current_id>'},
533
+ ):
534
+ json_or_form_data = _load_raw_data('json_or_form')
535
+ json_or_form_data['currentId'] = '<current_id>'
536
+
537
+ with app.test_request_context(
538
+ '/load_raw_data',
539
+ method='POST',
540
+ ):
541
+ json_or_form_data = _load_raw_data('json_or_form')
542
+ assert json_or_form_data == {}
543
+
544
+ # `path` location
545
+ @app.get('/load_raw_data/<int:pet_id>')
546
+ def load_raw_data(pet_id):
547
+ return _load_raw_data('path')
548
+
549
+ with app.test_request_context('/load_raw_data/1', method='GET'):
550
+ path_data = app.dispatch_request()
551
+ assert path_data == {'pet_id': 1}
@@ -1,6 +1,6 @@
1
1
  [tox]
2
2
  envlist =
3
- py3{12,11,10,9,8}
3
+ py3{13,12,11,10,9,8}
4
4
  pypy310
5
5
  minimal
6
6
  style
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes