Skip to content

Commit

Permalink
Merge pull request #7 from den4uk/dev
Browse files Browse the repository at this point in the history
merged datetime-iso8601
  • Loading branch information
den4uk committed Jul 10, 2019
2 parents 3746b30 + 216c773 commit 255ec1f
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 18 deletions.
41 changes: 33 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,44 @@ assert jsonextra.loads(my_json) == my_data # True


##### `.dump(obj, fp, **kwargs)` & `.dumps(obj, **kwargs)`
Will serialize extra data classes into their string representations (`__str__`).
Note: for _datetime_ objects, the `__str__` is dumped to an ISO8601-like format: `yyyy-mm-dd HH:MM:SS`, and it is the same format that will be expected by `.loads` method.
Will serialize extra data classes into their string (`__str__`) or special representations (_eg: `.isoformat`, etc._).


##### `.load(fp, **kwargs)` & `.loads(s, **kwargs)`
Will deserialize any stings, which match patterns of extra supported data classes. For example, if something looks like a _uuid_ - it will be converted to `uuid.UUID`.
Will deserialize any stings, which match patterns of extra supported data classes.
For example, if something looks like a _uuid_ - it will be converted to `uuid.UUID`.
If this behaviour is undesired, please use the built-in `json.loads` method instead of `jsonextra.loads`.



## Supported extra data classes

- `datetime.date`
- `datetime.datetime`
- `uuid.UUID`
- `bytes`
| Python Data Class | Python Object (deserialized) | JSON Object (serialized) |
|-------------------|------------------------------|--------------------------|
| `datetime.date` | `datetime.date(2019, 1, 1)` | `"2019-01-01"` |
| `datetime.time` | `datetime.time(23, 59, 11)` | `"23:59:11"` |
| `datetime.datetime` | `datetime.datetime(2019, 1, 1, 23, 59, 11)` | `"2019-01-01T23:59:11"` |
| `uuid.UUID` | `uuid.UUID('5f7660c5-88ea-46b6-93e2-860d5b7a0271')` | `"5f7660c5-88ea-46b6-93e2-860d5b7a0271"` |
| `bytes` | `b'\xd6aO\x1d\xd71Y\x05'` | `"base64:1mFPHdcxWQU="` |

More examples of serialized/deserialized values can be found in tests ;p


## Why?

_Why would you want to use this library?_

- If you work with a model-less data structures, and its data types expend beyond the JSON standard supports.
- If your model schemas are too dynamic to be able to use model-based serializers, and requires to store more data types.
- If your data structure does not fit with the JSON standard, and it needs expanding to support one or more data types.


## How it works

An extra supported python object is dumped to a _string_ value.
When loading a serialized json object, any values matching the string supported data class, will be converted to their expected data class instances.
> _If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck_.

## Contributions

Contibutions are welcome, please submit your pull requests into `dev` branch for a review.
12 changes: 9 additions & 3 deletions jsonextra/json_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
import json
import uuid
import base64
import datetime
import contextlib
import dateutil.parser

uuid_rex = re.compile(r'^[0-9a-f]{8}\-?[0-9a-f]{4}\-?4[0-9a-f]{3}\-?[89ab][0-9a-f]{3}\-?[0-9a-f]{12}$', re.I)
datetime_rex = re.compile(r'^\d{4}\-[01]\d\-[0-3]\d[\sT][0-2]\d\:[0-5]\d\:[0-5]\d')
date_rex = re.compile(r'^\d{4}\-[01]\d\-[0-3]\d$')
bytes_prefix = 'base64:'
bytes_rex = re.compile(r'^base64:([\w\d+/]*?\={,2}?)$')
time_rex = re.compile(r'^[0-2]\d\:[0-5]\d:[0-5]\d\.?\d{,6}?$')
BYTES_PREFIX = 'base64:'
bytes_rex = re.compile(BYTES_PREFIX + r'([\w\d\+/]*?\={,2}?)$', re.DOTALL)


class ExtraEncoder(json.JSONEncoder):
Expand All @@ -23,7 +25,9 @@ def default(self, obj):
return super().default(obj)
except TypeError:
if isinstance(obj, bytes):
return (bytes_prefix + self.bytes_to_b64(obj))
return (BYTES_PREFIX + self.bytes_to_b64(obj))
elif isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
return obj.isoformat()
return str(obj)


Expand All @@ -40,6 +44,8 @@ def _apply_extras(value: str):
return dateutil.parser.parse(value).date()
elif datetime_rex.match(value):
return dateutil.parser.parse(value)
elif time_rex.match(value):
return dateutil.parser.parse(value).time()
else:
try_bytes = bytes_rex.match(value)
if try_bytes:
Expand Down
9 changes: 9 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ files = jsonextra/__init__.py
tag = True
tag_name = {new_version}


[wheel]
universal = 1


[metadata]
license_file = LICENSE


[flake8]
ignore = E501
statistics = True
28 changes: 22 additions & 6 deletions tests/test_jsonextra.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
import io
import uuid
import secrets
import datetime
import jsonextra

Expand All @@ -10,7 +11,8 @@
'town': 'Hill Valley',
'episode': 2,
'date': datetime.date(2015, 10, 21),
'time': datetime.datetime(2019, 6, 14, 20, 43, 53, 207572),
'datetime': datetime.datetime(2019, 6, 14, 20, 43, 53, 207572),
'time': datetime.time(12, 34, 56),
'secret': b'\xd6aO\x1d\xd71Y\x05',
'anone': None,
'watch_again': True,
Expand All @@ -21,7 +23,8 @@
"town": "Hill Valley",
"episode": 2,
"date": "2015-10-21",
"time": "2019-06-14 20:43:53.207572",
"datetime": "2019-06-14T20:43:53.207572",
"time": "12:34:56",
"secret": "base64:1mFPHdcxWQU=",
"anone": null,
"watch_again": true
Expand Down Expand Up @@ -104,8 +107,12 @@ def test_dump(py_obj, json_obj):
({'x': None}, '{"x": null}'),
({'x': uuid.UUID('98f395f2-6ecb-46d8-98e4-926b8dfdd070')}, '{"x": "98f395f2-6ecb-46d8-98e4-926b8dfdd070"}'),
({'x': datetime.date(1991, 2, 16)}, '{"x": "1991-02-16"}'),
({'x': datetime.datetime(2001, 12, 1, 14, 58, 17)}, '{"x": "2001-12-01 14:58:17"}'),
({'x': datetime.datetime(2001, 12, 1, 14, 58, 17, 123456)}, '{"x": "2001-12-01 14:58:17.123456"}'),
({'x': datetime.datetime(2001, 12, 1, 14, 58, 17)}, '{"x": "2001-12-01T14:58:17"}'),
({'x': datetime.datetime(2001, 12, 1, 14, 58, 17, 123456)}, '{"x": "2001-12-01T14:58:17.123456"}'),
({'x': datetime.time(9, 12, 4)}, '{"x": "09:12:04"}'),
({'x': datetime.time(23, 52, 43)}, '{"x": "23:52:43"}'),
({'x': datetime.time(0)}, '{"x": "00:00:00"}'),
({'x': datetime.time(0, 1, 0, 1001)}, '{"x": "00:01:00.001001"}'),
({'x': b'hello'}, '{"x": "base64:aGVsbG8="}'),
({'x': b''}, '{"x": "base64:"}'),
]
Expand All @@ -122,10 +129,12 @@ def test_loads_many(py_obj, json_obj):


odd_cases = [
({'x': datetime.datetime(2019, 6, 16, 13, 31, 37)}, '{"x": "2019-06-16T13:31:37"}'), # Uses ISO8601 as input
({'x': datetime.datetime(2019, 6, 16, 13, 31, 37, 6399)}, '{"x": "2019-06-16T13:31:37.006399"}'), # Uses ISO8601 as input
({'x': datetime.datetime(2019, 6, 16, 13, 31, 37)}, '{"x": "2019-06-16 13:31:37"}'), # iso8601 without the `T`
({'x': datetime.datetime(2019, 6, 16, 13, 31, 37, 6399)}, '{"x": "2019-06-16 13:31:37.006399"}'), # iso8601 without the `T`
({'x': '24:23:22'}, '{"x": "24:23:22"}'), # Incorrect time
({'x': '2020-12-32'}, '{"x": "2020-12-32"}'), # Incorrect date
({'x': '2019-13-01 25:64:02'}, '{"x": "2019-13-01 25:64:02"}'), # Incorrect date/time
({'x': '2019-06-23 23:48:47 with some text'}, '{"x": "2019-06-23 23:48:47 with some text"}'), # Includes tailing text
({'x': '00000000-0000-0000-0000-000000000000'}, '{"x": "00000000-0000-0000-0000-000000000000"}'), # Not correctly structured guid
({'x': uuid.UUID('98f395f2-6ecb-46d8-98e4-926b8dfdd070')}, '{"x": "98F395F2-6ECB-46D8-98E4-926B8DFDD070"}'), # Uppercase
({'x': uuid.UUID('98f395f2-6ecb-46d8-98e4-926b8dfdd070')}, '{"x": "98f395f26ecb46d898e4926b8dfdd070"}'), # No dashes, but is valid
Expand All @@ -138,3 +147,10 @@ def test_loads_many(py_obj, json_obj):
@pytest.mark.parametrize('py_obj, json_obj', odd_cases)
def test_loads_odd_cases(py_obj, json_obj):
assert jsonextra.loads(json_obj) == py_obj


def test_random_bytes():
for n in range(128):
my_obj = {'x': secrets.token_bytes(n)}
serialized = jsonextra.dumps(my_obj)
assert jsonextra.loads(serialized) == my_obj
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ envlist = py3
deps =
-rrequirements-dev.txt
commands =
flake8 --ignore=E501 --statistics jsonextra/
flake8 jsonextra/
pytest --cov=jsonextra tests/
coverage html

Expand Down

0 comments on commit 255ec1f

Please sign in to comment.