Skip to content

Commit

Permalink
Adding documentation (#2)
Browse files Browse the repository at this point in the history
* Set up CI with Azure Pipelines

* add documentation

* update documentation and introduce basic tests
  • Loading branch information
danh91 authored May 29, 2019
1 parent 9ee687a commit 1245dea
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 32 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,5 @@ venv.bak/

# mypy
.mypy_cache/
.vscode/
.vscode/
.idea/
78 changes: 75 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,81 @@
# jstruct

JSON to struct to JSON
An eloquent and opinionated python library for nested object models definition offering simple serialization and deserialization into python dictionaries.

[![Build Status](https://dev.azure.com/danielkobina0854/danielkobina/_apis/build/status/DanH91.jstruct?branchName=master)](https://dev.azure.com/danielkobina0854/danielkobina/_build/latest?definitionId=1&branchName=master)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/cbe02771e00e42cd882ab48543782b40)](https://www.codacy.com/app/DanH91/jstruct?utm_source=github.com&utm_medium=referral&utm_content=DanH91/jstruct&utm_campaign=Badge_Grade)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

## Why

The deserialization of JSON or yaml into python data types is a common practice useful in many ways.
- Configuration file reading and writing
- REST API message response generation and request processing
- Object-Document Mapping for a document store
- Data import parsing or export generation

## How

`JStruct` leverage [attrs](https://www.attrs.org/en/stable/) the great `Classes without boilerplate` library to define structs without boilerplate.

## What

The result is a simple and intuitive syntax familiar to a pythonista that brings `Validation`, `Deserialization` and `Serialization`.

## Requirements

- Python 3.6 and +

## Installation

```bash
pip install -f https://git.io/purplship jstruct
Install using pip

```shell
pip install jstruct
```

## Usage

```python
import attr
from typing import List
from jstruct import struct, JList

@struct
class Person:
first_name: str
last_name: str

@struct
class RoleModels:
scientists: List[Person] = JList[Person]


payload = {
"scientists": [{"first_name": "John", "last_name": "Doe"}]
}

role_models = RoleModels(**payload)

print(role_models)

# RoleModels(scientists=[Person(first_name='John', last_name='Doe')])

print(attr.asdict(role_models))

# {'scientists': [{'first_name': 'John', 'last_name': 'Doe'}]}

```

## Authors

- **Daniel K.** - [@DanHK91](https://twitter.com/DanHK91) | [https://danielk.xyz](https://danielk.xyz/)

## Contribute

Contributors are welcomed.

## License

This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/DanH91/jstruct/blob/document-jstruct/LICENSE) file for details
22 changes: 22 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- master

pool:
vmImage: 'ubuntu-latest'

steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.6'
architecture: 'x64'

- script: |
source ./scripts.sh
init
test
displayName: 'Run tests'
34 changes: 28 additions & 6 deletions jstruct/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
from jstruct.types import (
JStruct,
JList,
JDict,
REQUIRED
)
# MIT License
#
# Copyright (c) 2019 Dan
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Import core names of jstruct.
from jstruct import struct, JStruct, JList, JDict
"""

from jstruct.types import struct, JStruct, JList, JDict, REQUIRED
135 changes: 116 additions & 19 deletions jstruct/types.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,159 @@
import attr
from functools import reduce
from typing import List, Dict
from typing import List, Dict, Union, Tuple, Optional


REQUIRED = True


@attr.s(auto_attribs=True)
def struct(*args, **kwargs):
"""a struct definition decorator for Python 3 datatypes like syntax.
:return the attrs.s funtion types
"""
return attr.s(auto_attribs=True, *args, **kwargs)


class _JStruct:
def __getitem__(self, arguments):
class_, required_, *kwargs = arguments if isinstance(arguments, tuple) else (arguments, False)
"""A typing definition wrapper used to defined nested struct.
@struct
class Child:
child_prop1: int
@struct
class Parent:
parent_prop: str
child: Child = JStruct[Child]
"""

def __getitem__(
self, arguments: Union[type, Tuple[type, Optional[bool], Optional[dict]]]
):
"""Override the `[]` operator to offer a typing wrapper syntactic sugar.
:arguments is either a `type` or a `tuple`
- type: the nested struct type (or class)
- tuple: ( type, REQUIRED, {dictionary of extra attr.ib arguments} )
:return a property initializer from attrs (attr.ib)
"""

class_, required_, *kwargs = (
arguments if isinstance(arguments, tuple) else (arguments, False)
)

def build(args) -> class_:
def converter(args) -> class_:
return class_(**args) if isinstance(args, dict) else args

default_ = dict(default=attr.NOTHING if required_ else None)
return attr.ib(
**default_,
converter=build,
converter=converter,
**dict(reduce(lambda r, d: r + list(d.items()), kwargs, []))
)


@attr.s(auto_attribs=True)
class _JList:
def __getitem__(self, arguments):
class_, required_, *kwargs = arguments if isinstance(arguments, tuple) else (arguments, False)
"""A typing definition wrapper used to defined nested collection (list) of struct.
@struct
class Child:
child_prop1: int
@struct
class Parent:
parent_prop: str
children: List[Child] = JList[Child]
"""

def __getitem__(
self, arguments: Union[type, Tuple[type, Optional[bool], Optional[dict]]]
):
"""Override the `[]` operator to offer a typing wrapper syntactic sugar.
:arguments is either a `type` or a `tuple`
- type: the nested struct type (or class)
- tuple: ( type, REQUIRED, {dictionary of extra attr.ib arguments} )
def build(args) -> List[class_]:
:return a property initializer from attrs (attr.ib)
"""

class_, required_, *kwargs = (
arguments if isinstance(arguments, tuple) else (arguments, False)
)

def converter(args) -> List[class_]:
if isinstance(args, list):
items = args
else:
items = [args]

return [
(class_(**item) if isinstance(item, dict) else item)
for item in items
(class_(**item) if isinstance(item, dict) else item) for item in items
]

default_ = dict(default=attr.NOTHING if required_ else [])

return attr.ib(
**default_,
converter=build,
converter=converter,
**dict(reduce(lambda r, d: r + list(d.items()), kwargs, []))
)


@attr.s(auto_attribs=True)
class _JDict:
def __getitem__(self, arguments):
key_type, value_type, required_, *kwargs = arguments if isinstance(arguments, tuple) else (arguments, False)
"""A typing definition wrapper used to defined nested dictionary struct typing.
from jstruct import struct
from jstruct.type import _JDict
JDict = _JDict()
@struct
class Child:
child_prop1: int
@struct
class Parent:
parent_prop: str
children: Dict[str, Child] = JDict[str, Child]
"""

def __getitem__(self, arguments: Tuple[type, type, Optional[bool], Optional[dict]]):
"""Override the `[]` operator to offer a typing wrapper syntactic sugar.
:arguments is a `tuple`
( key_type, value_type, REQUIRED, {dictionary of extra attr.ib arguments} )
:return a property initializer from attrs (attr.ib)
"""

def build(args) -> Dict[key_type, value_type]:
key_type, value_type, required_, *kwargs = (
arguments + (False,) if len(arguments) > 3 else arguments
)

def converter(args) -> Dict[key_type, value_type]:
return {
key_type(key): (value_type(**value) if isinstance(value, dict) else value)
key_type(key): (
value_type(**value) if isinstance(value, dict) else value
)
for (key, value) in args.items()
}

default_ = dict(default=attr.NOTHING if required_ else {})
return attr.ib(**default_, converter=build, **kwargs)
return attr.ib(
**default_,
converter=converter,
**dict(reduce(lambda r, d: r + list(d.items()), kwargs, []))
)


# Instance of _JStruct
JStruct = _JStruct()

# Instance of _JList
JList = _JList()

# Instance of _JDict
JDict = _JDict()
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest==4.5.0
black==19.3b0
18 changes: 18 additions & 0 deletions scripts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash

test() {
pytest test/
}

build() {
python setup.py sdist bdist_wheel
}

init() {
deactivate || true
rm -r ./venv || true
python3 -m venv ./venv &&
. ./venv/bin/activate &&
pip install -r requirements.txt &&
pip install -e .
}
13 changes: 10 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from setuptools import setup

setup(name='jstruct',
setup(
name='jstruct',
version='1.0.0',
description='Elegant JSON to python Data class',
description='Readable serializable and deserializable Python nested models',
url='https://github.com/DanH91/jstruct',
author='DanH91',
author_email='danielk.developer@gmail.com',
Expand All @@ -11,4 +12,10 @@
install_requires=[
'attrs==18.2.0'
],
zip_safe=False)
zip_safe=False,
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
)
Empty file added test/__init__.py
Empty file.
Loading

0 comments on commit 1245dea

Please sign in to comment.