Skip to content

Commit 4493b50

Browse files
authored
add UTC time zone always. (#37)
* add UTC time zone always.
1 parent bf7b494 commit 4493b50

File tree

10 files changed

+84
-62
lines changed

10 files changed

+84
-62
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## Version 0.20.0
2+
3+
* For date and time from `date-dime` format fields, the time zone is enforced set in the UTC.
4+
15
## Version 0.19.1
26

37
* Fix ref resolver.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Flask extension for using "specification first" and "API-first" principles.
1010
- [Settings](#settings)
1111
- [Tools](#tools)
1212
- [Data types](#data-types)
13+
- [`date-time` format](#date-time-format)
1314
- [Examples](#examples)
1415
- [Simple example](#simple-example)
1516
- [Specification from multiple file](#specification-from-multiple-file)
@@ -30,6 +31,7 @@ Flask extension for using "specification first" and "API-first" principles.
3031
* Provides a Swagger UI.
3132
* Support OpenAPI version 3.1.0.
3233
* Support specification from multiple file.
34+
* The time zone is always UTC.
3335

3436
## Installation
3537

@@ -79,6 +81,11 @@ Supported formats for string type field:
7981
* uri
8082
* binary
8183

84+
### `date-time` format
85+
86+
For `date-dime` format, the time zone is enforced set in the UTC. The time zone of the incoming
87+
DateTime object is discarded and set as UTC.
88+
8289
## Examples
8390

8491
### Simple example

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ license = {file = "LICENSE"}
2323
name = "Flask-First"
2424
readme = "README.md"
2525
requires-python = ">=3.9"
26-
version = "0.19.1"
26+
version = "0.20.0"
2727

2828
[project.optional-dependencies]
2929
dev = [

src/flask_first/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def add_request_validating() -> None:
141141
route_as_in_spec = self.route_to_openapi_format(route)
142142

143143
method = self._extract_method_from_request(request)
144-
params_schemas = self.spec.serialized_spec['paths'][route_as_in_spec][method].get(
144+
params_schemas = self.spec.deserialized_spec['paths'][route_as_in_spec][method].get(
145145
'parameters'
146146
)
147147
args = self._resolved_params(request.args)
@@ -188,7 +188,7 @@ def add_response_validating(response: Response) -> Response:
188188
route_as_in_spec = self.route_to_openapi_format(route)
189189

190190
try:
191-
route_schema: dict = self.spec.serialized_spec['paths'][route_as_in_spec]
191+
route_schema: dict = self.spec.deserialized_spec['paths'][route_as_in_spec]
192192
except KeyError as e:
193193
raise FirstResponseJSONValidation(
194194
f'Route <{e.args[0]}> not defined in specification.'

src/flask_first/first/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def __init__(
2828
json: dict = None,
2929
) -> None:
3030
self.spec = spec
31-
self._paths_schema = self.spec.serialized_spec['paths']
31+
self._paths_schema = self.spec.deserialized_spec['paths']
3232

3333
self.method = method.lower()
3434
self.endpoint = endpoint

src/flask_first/first/specification.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(
2525
self.raw_spec = load_from_yaml(self.path)
2626
self._validating_openapi_file(self.path, self.experimental_validator)
2727
self.resolved_spec = self._convert_parameters_to_schema(self.raw_spec)
28-
self.serialized_spec = self._convert_schemas(self.resolved_spec)
28+
self.deserialized_spec = self._convert_schemas(self.resolved_spec)
2929

3030
def _validating_openapi_file(self, path: Path, experimental_validator: bool):
3131
if experimental_validator:

src/flask_first/schema/schema_maker.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import timezone
12
from typing import Any
23
from typing import Optional
34

@@ -39,7 +40,7 @@ class HashmapField(fields.Field):
3940

4041
FIELDS_VIA_FORMATS = {
4142
'uuid': fields.UUID,
42-
'date-time': fields.DateTime,
43+
'date-time': fields.AwareDateTime,
4344
'date': fields.Date,
4445
'time': fields.Time,
4546
'email': fields.Email,
@@ -96,7 +97,9 @@ def _make_array_field(schema: dict, datetime_format: Optional[str] = None) -> fi
9697
field = nested_field
9798
elif data_format in FIELDS_VIA_FORMATS:
9899
if data_format == 'date-time':
99-
nested_field = FIELDS_VIA_FORMATS['date-time'](format=datetime_format)
100+
nested_field = FIELDS_VIA_FORMATS['date-time'](
101+
format=datetime_format, default_timezone=timezone.utc
102+
)
100103
else:
101104
nested_field = FIELDS_VIA_FORMATS[data_format]()
102105
field = fields.List(nested_field)
@@ -144,7 +147,9 @@ def make_marshmallow_schema(
144147
field = _make_multiple_field(schema['oneOf'], 'oneOf')
145148
elif schema.get('format'):
146149
if schema['format'] == 'date-time':
147-
field = FIELDS_VIA_FORMATS['date-time'](format=datetime_format)
150+
field = FIELDS_VIA_FORMATS['date-time'](
151+
format=datetime_format, default_timezone=timezone.utc
152+
)
148153
else:
149154
field = FIELDS_VIA_FORMATS[schema['format']]()
150155

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from datetime import datetime
2+
from pathlib import Path
3+
4+
import pytest
5+
from flask import request
6+
from flask_first.first.exceptions import FirstRequestJSONValidation
7+
from flask_first.first.exceptions import FirstResponseJSONValidation
8+
9+
from tests.conftest import BASEDIR
10+
11+
12+
def test_format_datetime(fx_create_app):
13+
def create_datetime() -> dict:
14+
datetime = request.extensions['first']['json']['datetime'].strftime("%Y-%m-%dT%H:%M:%S.%fZ")
15+
return {'datetime': datetime}
16+
17+
test_client = fx_create_app(
18+
Path(BASEDIR, 'specs/v3.1.0/datetime.openapi.yaml'), [create_datetime]
19+
)
20+
21+
json = {'datetime': datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")}
22+
r = test_client.post('/datetime', json=json)
23+
assert r.status_code == 200
24+
assert r.json['datetime'] == json['datetime']
25+
26+
27+
def test_request_datetime_error(fx_create_app):
28+
def create_datetime() -> dict:
29+
datetime = request.extensions['first']['json']['datetime']
30+
return {'datetime': datetime}
31+
32+
test_client = fx_create_app(
33+
Path(BASEDIR, 'specs/v3.1.0/datetime.openapi.yaml'), [create_datetime]
34+
)
35+
36+
json = {'datetime': datetime.utcnow().isoformat()}
37+
38+
with pytest.raises(FirstRequestJSONValidation):
39+
test_client.post('/datetime', json=json)
40+
41+
42+
def test_response_datetime_error(fx_create_app):
43+
def create_datetime() -> dict:
44+
datetime = request.extensions['first']['json']['datetime'].isoformat()
45+
return {'datetime': datetime}
46+
47+
test_client = fx_create_app(
48+
Path(BASEDIR, 'specs/v3.1.0/datetime.openapi.yaml'), [create_datetime]
49+
)
50+
51+
json = {'datetime': datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")}
52+
53+
with pytest.raises(FirstResponseJSONValidation):
54+
test_client.post('/datetime', json=json)

tests/test_flask_first.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,12 @@ def mini_endpoint(uuid_from_path: str, datetime_from_path: str) -> dict:
310310

311311
assert isinstance(uuid_from_query, uuid.UUID)
312312
assert uuid_from_path == str(uuid_from_query)
313-
assert datetime_from_path == str(datetime_from_query).replace(' ', 'T')
313+
assert datetime_from_path == datetime_from_query.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
314314

315315
return {
316316
'uuid_from_path': uuid_from_path,
317317
'uuid_from_query': str(uuid_from_query),
318-
'datetime_from_query': str(datetime_from_query).replace(' ', 'T'),
318+
'datetime_from_query': datetime_from_query.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
319319
}
320320

321321
first = First(Path(BASEDIR, 'specs/v3.1.0/parameters.openapi.yaml'))
@@ -325,6 +325,7 @@ def create_app():
325325
app.debug = 1
326326
app.testing = 1
327327
app.config['FIRST_RESPONSE_VALIDATION'] = True
328+
app.config['FIRST_DATETIME_FORMAT'] = "%Y-%m-%dT%H:%M:%S.%fZ"
328329
first.init_app(app)
329330
first.add_view_func(mini_endpoint)
330331
return app
@@ -333,10 +334,10 @@ def create_app():
333334

334335
with app.test_client() as test_client:
335336
test_uuid = str(uuid.uuid4())
336-
test_datetime = '2021-12-24T18:32:05'
337+
test_datetime = '2021-12-24T18:32:05.123456Z'
338+
query_string = {'uuid_from_query': test_uuid, 'datetime_from_query': test_datetime}
337339
r = test_client.get(
338-
f'/parameters_endpoint/{test_uuid}/{test_datetime}',
339-
query_string={'uuid_from_query': test_uuid, 'datetime_from_query': test_datetime},
340+
f'/parameters_endpoint/{test_uuid}/{test_datetime}', query_string=query_string
340341
)
341342
assert r.status_code == 200
342343
assert r.json == {

tests/test_responses.py

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
from datetime import datetime
21
from pathlib import Path
32

4-
import pytest
53
from flask import request
6-
from flask_first.first.exceptions import FirstRequestJSONValidation
7-
from flask_first.first.exceptions import FirstResponseJSONValidation
84

95
from .conftest import BASEDIR
106

@@ -20,48 +16,3 @@ def default_responses() -> dict:
2016
r = test_client.post('/message', json={'message': 'OK'})
2117
assert r.status_code == 200
2218
assert r.json['message'] == 'OK'
23-
24-
25-
def test_responses__datetime(fx_create_app):
26-
def create_datetime() -> dict:
27-
datetime = request.extensions['first']['json']['datetime'].strftime("%Y-%m-%dT%H:%M:%S.%fZ")
28-
return {'datetime': datetime}
29-
30-
test_client = fx_create_app(
31-
Path(BASEDIR, 'specs/v3.1.0/datetime.openapi.yaml'), [create_datetime]
32-
)
33-
34-
json = {'datetime': datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")}
35-
r = test_client.post('/datetime', json=json)
36-
assert r.status_code == 200
37-
assert r.json['datetime'] == json['datetime']
38-
39-
40-
def test_responses__request_datetime_error(fx_create_app):
41-
def create_datetime() -> dict:
42-
datetime = request.extensions['first']['json']['datetime']
43-
return {'datetime': datetime}
44-
45-
test_client = fx_create_app(
46-
Path(BASEDIR, 'specs/v3.1.0/datetime.openapi.yaml'), [create_datetime]
47-
)
48-
49-
json = {'datetime': datetime.utcnow().isoformat()}
50-
51-
with pytest.raises(FirstRequestJSONValidation):
52-
test_client.post('/datetime', json=json)
53-
54-
55-
def test_responses__response_datetime_error(fx_create_app):
56-
def create_datetime() -> dict:
57-
datetime = request.extensions['first']['json']['datetime'].isoformat()
58-
return {'datetime': datetime}
59-
60-
test_client = fx_create_app(
61-
Path(BASEDIR, 'specs/v3.1.0/datetime.openapi.yaml'), [create_datetime]
62-
)
63-
64-
json = {'datetime': datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")}
65-
66-
with pytest.raises(FirstResponseJSONValidation):
67-
test_client.post('/datetime', json=json)

0 commit comments

Comments
 (0)