Skip to content

Commit b5e54ad

Browse files
committed
ISO date time parsing
1 parent 194fc4c commit b5e54ad

File tree

9 files changed

+145
-10
lines changed

9 files changed

+145
-10
lines changed

TODO.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232
- [ ] Complex python types annotations
3333
- [ ] Decorator to specify field metatype
3434
- [ ] Specify metatype in attr/dataclass argument (if dataclasses has such)
35-
- [ ] String based types
36-
- [ ] ISO date
37-
- [ ] ISO time
38-
- [ ] ISO datetime
35+
- [ ] String based types (Warning: 6 times slow down)
36+
- [X] ISO date
37+
- [X] ISO time
38+
- [X] ISO datetime
3939
- API Layer
4040
- [ ] Route object
4141
- [ ] Register model as route in/out data spec
@@ -68,6 +68,10 @@
6868
- Generate OpenAPI spec
6969
- [ ] Meta-model -> OpenAPI model converter
7070
- [ ] Route -> OpenAPI converter
71+
- [ ] String based types
72+
- [ ] ISO date
73+
- [ ] ISO time
74+
- [ ] ISO datetime
7175

7276
- Build, Deploy, CI
7377
- [ ] setup.py

rest_client_gen/dynamic_typing/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
)
44
from .complex import ComplexType, DList, DOptional, DTuple, DUnion, SingleType
55
from .models_meta import AbsoluteModelRef, ModelMeta, ModelPtr
6+
from .string_datetime import IsoDateString, IsoDatetimeString, IsoTimeString, register_datetime_classes
67
from .string_serializable import (
78
BooleanString, FloatString, IntString, StringSerializable, StringSerializableRegistry, registry
89
)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import operator
2+
from datetime import date, datetime, time
3+
from typing import Optional
4+
5+
import dateutil.parser
6+
7+
from .string_serializable import StringSerializable, StringSerializableRegistry, registry
8+
9+
_dt_args_getter = operator.attrgetter('year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond', 'tzinfo')
10+
_d_args_getter = operator.attrgetter('year', 'month', 'day')
11+
_t_args_getter = operator.attrgetter('hour', 'minute', 'second', 'microsecond', 'tzinfo')
12+
13+
14+
def _extend_datetime(d, cls: type):
15+
if isinstance(d, datetime):
16+
args = _dt_args_getter
17+
elif isinstance(d, time):
18+
args = _t_args_getter
19+
else:
20+
args = _d_args_getter
21+
return cls(*args(d))
22+
23+
24+
_check_values_date = (
25+
datetime(2018, 1, 2, 0, 4, 5, 678, tzinfo=None),
26+
datetime(2018, 1, 2, 9, 4, 5, 678, tzinfo=None)
27+
)
28+
29+
30+
def is_date(s: str) -> Optional[date]:
31+
d1 = dateutil.parser.parse(s, default=_check_values_date[0])
32+
d2 = dateutil.parser.parse(s, default=_check_values_date[1])
33+
return None if d1 == d2 else d1.date()
34+
35+
36+
_check_values_time = (
37+
datetime(2018, 10, 11, 0, 4, 5, 678, tzinfo=None),
38+
datetime(2018, 12, 30, 0, 4, 5, 678, tzinfo=None)
39+
)
40+
41+
42+
def is_time(s: str) -> Optional[time]:
43+
d1 = dateutil.parser.parse(s, default=_check_values_time[0])
44+
d2 = dateutil.parser.parse(s, default=_check_values_time[1])
45+
return None if d1 == d2 else d1.time()
46+
47+
48+
class IsoDateString(StringSerializable, date):
49+
"""
50+
Parse date using dateutil.parser.isoparse. Representation format always is ``YYYY-MM-DD``.
51+
You can override to_representation method to customize it. Just don't forget to call registry.remove(YourCls)
52+
"""
53+
54+
@classmethod
55+
def to_internal_value(cls, value: str) -> 'IsoDateString':
56+
if not is_date(value):
57+
raise ValueError(f"'{value}' is not valid date")
58+
dt = dateutil.parser.isoparse(value)
59+
return _extend_datetime(dt.date(), cls)
60+
61+
def to_representation(self):
62+
return self.isoformat()
63+
64+
def replace(self, *args, **kwargs) -> 'IsoDateString':
65+
# noinspection PyTypeChecker
66+
return date.replace(self, *args, **kwargs)
67+
68+
69+
class IsoTimeString(StringSerializable, time):
70+
"""
71+
Parse time using dateutil.parser.parse. Representation format always is ``hh:mm:ss.ms``.
72+
You can override to_representation method to customize it.
73+
"""
74+
75+
@classmethod
76+
def to_internal_value(cls, value: str) -> 'IsoTimeString':
77+
t = is_time(value)
78+
if not t:
79+
raise ValueError(f"'{value}' is not valid time")
80+
return _extend_datetime(t, cls)
81+
82+
def to_representation(self):
83+
return self.isoformat()
84+
85+
def replace(self, *args, **kwargs) -> 'IsoTimeString':
86+
# noinspection PyTypeChecker
87+
return time.replace(self, *args, **kwargs)
88+
89+
90+
class IsoDatetimeString(StringSerializable, datetime):
91+
"""
92+
Parse datetime using dateutil.parser.isoparse.
93+
Representation format always is ``YYYY-MM-DDThh:mm:ss.ms`` (datetime.isoformat method).
94+
"""
95+
96+
@classmethod
97+
def to_internal_value(cls, value: str) -> 'IsoDatetimeString':
98+
dt = dateutil.parser.isoparse(value)
99+
return _extend_datetime(dt, cls)
100+
101+
def to_representation(self):
102+
return self.isoformat()
103+
104+
def replace(self, *args, **kwargs) -> 'IsoDatetimeString':
105+
# noinspection PyTypeChecker
106+
return datetime.replace(self, *args, **kwargs)
107+
108+
109+
def register_datetime_classes(registry: StringSerializableRegistry = registry):
110+
"""
111+
Register datetime classes in given registry (using default registry if no arguments is passed).
112+
Date parsing is expensive operation so this classes are disabled by default
113+
"""
114+
registry.add(cls=IsoDateString)
115+
registry.add(cls=IsoTimeString)
116+
registry.add(cls=IsoDatetimeString)

rest_client_gen/dynamic_typing/string_serializable.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def to_typing_code(cls) -> Tuple[ImportPathList, str]:
3434
as a metadata instance but contains actual data
3535
"""
3636
cls_name = cls.__name__
37-
return [('rest_client_gen.dynamic_typing.string_serializable', cls_name)], cls_name
37+
return [('rest_client_gen.dynamic_typing', cls_name)], cls_name
3838

3939

4040
T_StringSerializable = Type[StringSerializable]
@@ -71,6 +71,17 @@ def decorator(cls):
7171

7272
return decorator
7373

74+
def remove(self, cls: type):
75+
"""
76+
Unregister given class
77+
78+
:param cls: StringSerializable class
79+
"""
80+
self.types.remove(cls)
81+
for base, replace in list(self.replaces):
82+
if replace is cls:
83+
self.replaces.remove((base, replace))
84+
7485
def resolve(self, *types: T_StringSerializable) -> Collection[T_StringSerializable]:
7586
"""
7687
Return set of StringSerializable classes which can represent all classes from types argument.

test/test_code_generation/test_attrs_generation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ class Test:
119119
"generated": trim(f"""
120120
import attr
121121
from attr.converter import optional
122-
from rest_client_gen.dynamic_typing.string_serializable import FloatString, IntString
122+
from rest_client_gen.dynamic_typing import FloatString, IntString
123123
from typing import List, Optional
124124
125125

test/test_code_generation/test_models_code_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class Test:
118118
}
119119
},
120120
"fields": {
121-
"imports": "from rest_client_gen.dynamic_typing.string_serializable import IntString\n"
121+
"imports": "from rest_client_gen.dynamic_typing import IntString\n"
122122
"from typing import List, Optional",
123123
"fields": [
124124
"foo: int",
@@ -127,7 +127,7 @@ class Test:
127127
]
128128
},
129129
"generated": trim("""
130-
from rest_client_gen.dynamic_typing.string_serializable import IntString
130+
from rest_client_gen.dynamic_typing import IntString
131131
from typing import List, Optional
132132
133133

test/test_code_generation/test_typing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@ class TestModel:
104104
),
105105
pytest.param(
106106
FloatString,
107-
('from rest_client_gen.dynamic_typing.string_serializable import FloatString', FloatString),
107+
('from rest_client_gen.dynamic_typing import FloatString', FloatString),
108108
id="string_serializable"
109109
),
110110
pytest.param(
111111
DOptional(IntString),
112-
('from rest_client_gen.dynamic_typing.string_serializable import IntString\n'
112+
('from rest_client_gen.dynamic_typing import IntString\n'
113113
'from typing import Optional', Optional[IntString]),
114114
id="complex_string_serializable"
115115
),

testing_tools/real_apis/f1.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import inflection
66
import requests
77

8+
from rest_client_gen.dynamic_typing import register_datetime_classes
89
from rest_client_gen.generator import MetadataGenerator
910
from rest_client_gen.models import compose_models
1011
from rest_client_gen.models.attr import AttrsModelCodeGenerator
@@ -43,6 +44,7 @@ def main():
4344
dump_response("f1", "driver_standings", driver_standings_data)
4445
driver_standings_data = ("driver_standings", driver_standings_data)
4546

47+
register_datetime_classes()
4648
gen = MetadataGenerator()
4749
reg = ModelRegistry()
4850
for name, data in (results_data, drivers_data, driver_standings_data):

testing_tools/real_apis/pathofexile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def main():
2626

2727
print(f"Start model generation (data len = {len(tabs)})")
2828
start_t = datetime.now()
29+
# register_datetime_classes()
2930
gen = MetadataGenerator()
3031
reg = ModelRegistry()
3132
fields = gen.generate(*tabs)

0 commit comments

Comments
 (0)