nvelope


Namenvelope JSON
Version 1.1.1 PyPI version JSON
download
home_pagehttps://github.com/monomonedula/nvelope
SummaryPainless JSON marshalling and unmarshalling
upload_time2023-09-23 09:55:02
maintainer
docs_urlNone
authormonomonedula
requires_python>=3.8,<4.0
licenseMIT
keywords serialization deserialization json marshalling unmarshalling utility
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI
coveralls test coverage No coveralls.
            [![codecov](https://codecov.io/gh/monomonedula/nvelope/branch/master/graph/badge.svg?token=yunFiDdUEK)](https://codecov.io/gh/monomonedula/nvelope)
[![Build Status](https://app.travis-ci.com/monomonedula/nvelope.svg?branch=master)](https://app.travis-ci.com/monomonedula/nvelope)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Downloads](https://pepy.tech/badge/nvelope)](https://pepy.tech/project/nvelope)
# nvelope

Define your JSON schema as Python dataclasses

It's kinda like Pydantic but better.

Now with JSON-schema generation!

## Installation
`pip install nvelope`


## The problem it solves

With `nvelope` you can define dataclasses which know how to convert themselves from/to JSON.
All with custom checks and custom defined conversions from/to JSON for any type you want to put into your dataclass.

This library was designed with extensibility in mind, 
so it relies on interfaces (for the most part) rather than 
some weird inheritance stuff and other magic.

You can (and probably should) take a look at the code! 
The code base is microscopic compared to Pydantic.



## Usage

Say you have a JSON representing a user in your app looking something like this
```json
{
    "id": 530716139,
    "username": "johndoe",
    "language_code": "en"
}
```

You define an envelope for it

```python
from dataclasses import dataclass

from nvelope import (Obj, int_conv, string_conv)

@dataclass      # note the @dataclass decorator, it is important
class User(Obj):
    _conversion = {
        "id": int_conv,
        "language_code": string_conv,
        "username": string_conv,
    }

    id: int
    language_code: str
    username: str

```


Now you have a model that knows how to read data from the JSON 
(not the raw string, actually, but to the types that are allowed by the
standard `json.dumps` function e.g. `dict`, `list`, `str`, `int`, `float`, `bool`, `None` ) ...

```python
user = User.from_json(
    {
        "id": 530716139,
        "username": "johndoe",
        "language_code": "en"
    }
)
```
... and knows how to convert itself into JSON 

```python
User(
    id=530716139,
    username="johndoe",
    language_code="en",
).as_json() 

# returns a dictionary {
#     "id": 530716139,
#     "username": "johndoe",
#     "language_code": "en"
# }
```


### Compound envelopes
You can also define compound envelopes.

Say we want to define a message and include info about the sender.
Having defined the `User` envelope, we can do it like this:

```python
from nvelope import CompoundConv

@dataclass
class Message(Obj):
    _conversion = {
        "message_id": int_conv,
        "from_": CompoundConv(User),
        "text": string_conv,
    }

    from_: User
    text: str
    message_id: int
```
and use it the same way:

```python
# reading an obj from parsed json like this

Message.from_json(
    {
        "message_id": 44,
        "text": "hello there",
        "from_": {
            "id": 530716139,
            "username": "johndoe",
            "language_code": "en"
        }
    }
)

# and dumping an object to json like this

import json

json.dumps(
    Message(
        message_id=44,
        text="whatever",
        from_=User(
            id=530716139,
            username="johndoe",
            language_code="en",
        )
    ).as_json()
)
```


### Arrays

This is how you define arrays:

```python
from nvelope import Arr, CompoundConv


class Users(Arr):
    conversion = CompoundConv(User)


# Same API inherited from nvelope.Compound interface

Users.from_json(
    [
        {
            "id": 530716139,
            "username": "johndoe",
            "language_code": "en",
        },
        {
            "id": 452341341,
            "username": "ivandrago",
            "language_code": "ru",
        }
    ]
)

Users(
    [
        User(
            id=530716139,
            username="johndoe",
            language_code="en",
        ),
        User(
            id=452341341,
            username="ivandrago",
            language_code="ru",
        ),
    ]
).as_json()
```

### Field aliases

At some point you may need to define an envelope for an API containing certain field names which cannot be
used in python since they are reserved keywords (such as `def`, `from`, etc.).

There's a solution for this:

```python
from dataclasses import dataclass
from nvelope import Obj, string_conv, CompoundConv, AliasTable

@dataclass
class Comment(Obj):
    _conversion = {
        "text": string_conv,
        "from_": CompoundConv(User),
    }
    
    
    _alias_table = AliasTable({"from_": "from"})
            
    text: str
    from_: User

```

In this case `from` key gets replaced by `from_` in the python model. 
The `from_` field gets translated back to `from` when calling `.as_json()`

### Missing and optional fields

There's a difference between fields that can be set to `None` and fields which may be missing in the JSON at all.

This is how you specify that a some field may be missing from the JSON and that's OK:
```python
from dataclasses import dataclass
from typing import Optional

from nvelope import MaybeMissing, Obj, OptionalConv, AliasTable

@dataclass
class Comment(Obj):
    _alias_table = AliasTable(
        {"from_": "from"}
    )
    
    text: str
    img: Optional[str]          # this field can be set to None (null), but is must always be present in the JSON
    from_: MaybeMissing[User]   # this field can be missing from JSON body

    _conversion = {
        "text": string_conv,
        "img": OptionalConv(string_conv),   # note the wrapping with OptionalConv
        "from_": CompoundConv(User),
    }

```

This is how you check if the `MaybeMissing` field is actually missing
```python
comment.from_.has()     # returns False if the field is missing
```

and this is how you get the value:
```python
comment.value()     # raises an error if there's no value, 
                    # so it is recommended to check the output of .has()
                    #  before calling .value() 
```

### Json-schema support
The `Comment` model from we have defined generates schema like this:
```python
    Comment.schema()
```

with the returned schema looking like this:
```python
{
    "type": "object",
    "properties": {
        "from": {
            "properties": {
                "id": {"type": "integer"},
                "language_code": {"type": "string"},
                "username": {"type": "string"},
            },
            "required": ["id", "language_code", "username"],
            "type": "object",
        },
        "img": {"type": ["string", "null"]},
        "text": {"type": "string"},
    },
    "required": ["text", "img"],
}
```
**NOTE**: `nvelope` does not perform json schema checks.

### Custom conversions


You may define a custom conversions inheriting from `nvelope.nvelope.Conversion` abstract base class 
or using `nvelope.nvelope.ConversionOf` class. 

For example, this is how `datetime_iso_format_conv` is defined:

```python
from nvelope import WithTypeCheckOnDump, ConversionOf

datetime_iso_format_conv = WithTypeCheckOnDump(
    datetime.datetime,
    ConversionOf(
        to_json=lambda v: v.isoformat(),
        from_json=lambda s: datetime.datetime.fromisoformat(s),
    ),
)

```

Say we want to jsonify a `datetime` field as POSIX timestamp, instead of storing it in ISO string format.

```python
datetime_timestamp_conv = ConversionOf(
    to_json=lambda v: v.timestamp(),
    from_json=lambda s: datetime.datetime.fromtimestamp(s),
    schema={"type": "number"},
)
```

We could also add `WithTypeCheckOnDump` wrapper in order to add explicit check that 
the value passed into `.from_json()`
is indeed `float`.

```python
from nvelope import ConversionOf

datetime_timestamp_conv = WithTypeCheckOnDump(
    float,
    ConversionOf(
        to_json=lambda v: v.timestamp(),
        from_json=lambda s: datetime.datetime.fromtimestamp(s),
        schema={"type": "number"},
    )
)
```

You may also go further and implement custom conversion.
Inherit from `nvelope.Conversion` interface, implement its abstract methods, and you are good to go.


### Custom compounds

You can also define custom alternatives to `nvelope.Obj` and `nvelope.Arr`.
It will work fine as long as they inherit `nvelope.Compound` interface.

It currently required 3 methods:
- `from_json` 
- `as_json`
- `schema`

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/monomonedula/nvelope",
    "name": "nvelope",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8,<4.0",
    "maintainer_email": "",
    "keywords": "serialization,deserialization,json,marshalling,unmarshalling,utility",
    "author": "monomonedula",
    "author_email": "valh@tuta.io",
    "download_url": "https://files.pythonhosted.org/packages/46/95/c1c5e0ca56e2b844b7a64173b897445c4be51f63a1eeabed53a20f0c703c/nvelope-1.1.1.tar.gz",
    "platform": null,
    "description": "[![codecov](https://codecov.io/gh/monomonedula/nvelope/branch/master/graph/badge.svg?token=yunFiDdUEK)](https://codecov.io/gh/monomonedula/nvelope)\n[![Build Status](https://app.travis-ci.com/monomonedula/nvelope.svg?branch=master)](https://app.travis-ci.com/monomonedula/nvelope)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![Downloads](https://pepy.tech/badge/nvelope)](https://pepy.tech/project/nvelope)\n# nvelope\n\nDefine your JSON schema as Python dataclasses\n\nIt's kinda like Pydantic but better.\n\nNow with JSON-schema generation!\n\n## Installation\n`pip install nvelope`\n\n\n## The problem it solves\n\nWith `nvelope` you can define dataclasses which know how to convert themselves from/to JSON.\nAll with custom checks and custom defined conversions from/to JSON for any type you want to put into your dataclass.\n\nThis library was designed with extensibility in mind, \nso it relies on interfaces (for the most part) rather than \nsome weird inheritance stuff and other magic.\n\nYou can (and probably should) take a look at the code! \nThe code base is microscopic compared to Pydantic.\n\n\n\n## Usage\n\nSay you have a JSON representing a user in your app looking something like this\n```json\n{\n    \"id\": 530716139,\n    \"username\": \"johndoe\",\n    \"language_code\": \"en\"\n}\n```\n\nYou define an envelope for it\n\n```python\nfrom dataclasses import dataclass\n\nfrom nvelope import (Obj, int_conv, string_conv)\n\n@dataclass      # note the @dataclass decorator, it is important\nclass User(Obj):\n    _conversion = {\n        \"id\": int_conv,\n        \"language_code\": string_conv,\n        \"username\": string_conv,\n    }\n\n    id: int\n    language_code: str\n    username: str\n\n```\n\n\nNow you have a model that knows how to read data from the JSON \n(not the raw string, actually, but to the types that are allowed by the\nstandard `json.dumps` function e.g. `dict`, `list`, `str`, `int`, `float`, `bool`, `None` ) ...\n\n```python\nuser = User.from_json(\n    {\n        \"id\": 530716139,\n        \"username\": \"johndoe\",\n        \"language_code\": \"en\"\n    }\n)\n```\n... and knows how to convert itself into JSON \n\n```python\nUser(\n    id=530716139,\n    username=\"johndoe\",\n    language_code=\"en\",\n).as_json() \n\n# returns a dictionary {\n#     \"id\": 530716139,\n#     \"username\": \"johndoe\",\n#     \"language_code\": \"en\"\n# }\n```\n\n\n### Compound envelopes\nYou can also define compound envelopes.\n\nSay we want to define a message and include info about the sender.\nHaving defined the `User` envelope, we can do it like this:\n\n```python\nfrom nvelope import CompoundConv\n\n@dataclass\nclass Message(Obj):\n    _conversion = {\n        \"message_id\": int_conv,\n        \"from_\": CompoundConv(User),\n        \"text\": string_conv,\n    }\n\n    from_: User\n    text: str\n    message_id: int\n```\nand use it the same way:\n\n```python\n# reading an obj from parsed json like this\n\nMessage.from_json(\n    {\n        \"message_id\": 44,\n        \"text\": \"hello there\",\n        \"from_\": {\n            \"id\": 530716139,\n            \"username\": \"johndoe\",\n            \"language_code\": \"en\"\n        }\n    }\n)\n\n# and dumping an object to json like this\n\nimport json\n\njson.dumps(\n    Message(\n        message_id=44,\n        text=\"whatever\",\n        from_=User(\n            id=530716139,\n            username=\"johndoe\",\n            language_code=\"en\",\n        )\n    ).as_json()\n)\n```\n\n\n### Arrays\n\nThis is how you define arrays:\n\n```python\nfrom nvelope import Arr, CompoundConv\n\n\nclass Users(Arr):\n    conversion = CompoundConv(User)\n\n\n# Same API inherited from nvelope.Compound interface\n\nUsers.from_json(\n    [\n        {\n            \"id\": 530716139,\n            \"username\": \"johndoe\",\n            \"language_code\": \"en\",\n        },\n        {\n            \"id\": 452341341,\n            \"username\": \"ivandrago\",\n            \"language_code\": \"ru\",\n        }\n    ]\n)\n\nUsers(\n    [\n        User(\n            id=530716139,\n            username=\"johndoe\",\n            language_code=\"en\",\n        ),\n        User(\n            id=452341341,\n            username=\"ivandrago\",\n            language_code=\"ru\",\n        ),\n    ]\n).as_json()\n```\n\n### Field aliases\n\nAt some point you may need to define an envelope for an API containing certain field names which cannot be\nused in python since they are reserved keywords (such as `def`, `from`, etc.).\n\nThere's a solution for this:\n\n```python\nfrom dataclasses import dataclass\nfrom nvelope import Obj, string_conv, CompoundConv, AliasTable\n\n@dataclass\nclass Comment(Obj):\n    _conversion = {\n        \"text\": string_conv,\n        \"from_\": CompoundConv(User),\n    }\n    \n    \n    _alias_table = AliasTable({\"from_\": \"from\"})\n            \n    text: str\n    from_: User\n\n```\n\nIn this case `from` key gets replaced by `from_` in the python model. \nThe `from_` field gets translated back to `from` when calling `.as_json()`\n\n### Missing and optional fields\n\nThere's a difference between fields that can be set to `None` and fields which may be missing in the JSON at all.\n\nThis is how you specify that a some field may be missing from the JSON and that's OK:\n```python\nfrom dataclasses import dataclass\nfrom typing import Optional\n\nfrom nvelope import MaybeMissing, Obj, OptionalConv, AliasTable\n\n@dataclass\nclass Comment(Obj):\n    _alias_table = AliasTable(\n        {\"from_\": \"from\"}\n    )\n    \n    text: str\n    img: Optional[str]          # this field can be set to None (null), but is must always be present in the JSON\n    from_: MaybeMissing[User]   # this field can be missing from JSON body\n\n    _conversion = {\n        \"text\": string_conv,\n        \"img\": OptionalConv(string_conv),   # note the wrapping with OptionalConv\n        \"from_\": CompoundConv(User),\n    }\n\n```\n\nThis is how you check if the `MaybeMissing` field is actually missing\n```python\ncomment.from_.has()     # returns False if the field is missing\n```\n\nand this is how you get the value:\n```python\ncomment.value()     # raises an error if there's no value, \n                    # so it is recommended to check the output of .has()\n                    #  before calling .value() \n```\n\n### Json-schema support\nThe `Comment` model from we have defined generates schema like this:\n```python\n    Comment.schema()\n```\n\nwith the returned schema looking like this:\n```python\n{\n    \"type\": \"object\",\n    \"properties\": {\n        \"from\": {\n            \"properties\": {\n                \"id\": {\"type\": \"integer\"},\n                \"language_code\": {\"type\": \"string\"},\n                \"username\": {\"type\": \"string\"},\n            },\n            \"required\": [\"id\", \"language_code\", \"username\"],\n            \"type\": \"object\",\n        },\n        \"img\": {\"type\": [\"string\", \"null\"]},\n        \"text\": {\"type\": \"string\"},\n    },\n    \"required\": [\"text\", \"img\"],\n}\n```\n**NOTE**: `nvelope` does not perform json schema checks.\n\n### Custom conversions\n\n\nYou may define a custom conversions inheriting from `nvelope.nvelope.Conversion` abstract base class \nor using `nvelope.nvelope.ConversionOf` class. \n\nFor example, this is how `datetime_iso_format_conv` is defined:\n\n```python\nfrom nvelope import WithTypeCheckOnDump, ConversionOf\n\ndatetime_iso_format_conv = WithTypeCheckOnDump(\n    datetime.datetime,\n    ConversionOf(\n        to_json=lambda v: v.isoformat(),\n        from_json=lambda s: datetime.datetime.fromisoformat(s),\n    ),\n)\n\n```\n\nSay we want to jsonify a `datetime` field as POSIX timestamp, instead of storing it in ISO string format.\n\n```python\ndatetime_timestamp_conv = ConversionOf(\n    to_json=lambda v: v.timestamp(),\n    from_json=lambda s: datetime.datetime.fromtimestamp(s),\n    schema={\"type\": \"number\"},\n)\n```\n\nWe could also add `WithTypeCheckOnDump` wrapper in order to add explicit check that \nthe value passed into `.from_json()`\nis indeed `float`.\n\n```python\nfrom nvelope import ConversionOf\n\ndatetime_timestamp_conv = WithTypeCheckOnDump(\n    float,\n    ConversionOf(\n        to_json=lambda v: v.timestamp(),\n        from_json=lambda s: datetime.datetime.fromtimestamp(s),\n        schema={\"type\": \"number\"},\n    )\n)\n```\n\nYou may also go further and implement custom conversion.\nInherit from `nvelope.Conversion` interface, implement its abstract methods, and you are good to go.\n\n\n### Custom compounds\n\nYou can also define custom alternatives to `nvelope.Obj` and `nvelope.Arr`.\nIt will work fine as long as they inherit `nvelope.Compound` interface.\n\nIt currently required 3 methods:\n- `from_json` \n- `as_json`\n- `schema`\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Painless JSON marshalling and unmarshalling",
    "version": "1.1.1",
    "project_urls": {
        "Homepage": "https://github.com/monomonedula/nvelope",
        "Repository": "https://github.com/monomonedula/nvelope"
    },
    "split_keywords": [
        "serialization",
        "deserialization",
        "json",
        "marshalling",
        "unmarshalling",
        "utility"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "f62ed1477cb7001b660943ef830f395414f064fcd668195cf9f937aab36965fd",
                "md5": "fd9fec4fcf09890e185ad2aef8ad0b6a",
                "sha256": "277635be2a081acd1ffe4d13ef34b4372fd5f4cdb96ad09a4e995b79fd5f68d1"
            },
            "downloads": -1,
            "filename": "nvelope-1.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "fd9fec4fcf09890e185ad2aef8ad0b6a",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8,<4.0",
            "size": 10491,
            "upload_time": "2023-09-23T09:55:00",
            "upload_time_iso_8601": "2023-09-23T09:55:00.390093Z",
            "url": "https://files.pythonhosted.org/packages/f6/2e/d1477cb7001b660943ef830f395414f064fcd668195cf9f937aab36965fd/nvelope-1.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "4695c1c5e0ca56e2b844b7a64173b897445c4be51f63a1eeabed53a20f0c703c",
                "md5": "32f586be5a382c8f6ea8177ebd64df15",
                "sha256": "afce8f947d0b7000781d525432cc234a6b2b95c8f00bcba585b465c33a9b1ec9"
            },
            "downloads": -1,
            "filename": "nvelope-1.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "32f586be5a382c8f6ea8177ebd64df15",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8,<4.0",
            "size": 13077,
            "upload_time": "2023-09-23T09:55:02",
            "upload_time_iso_8601": "2023-09-23T09:55:02.775480Z",
            "url": "https://files.pythonhosted.org/packages/46/95/c1c5e0ca56e2b844b7a64173b897445c4be51f63a1eeabed53a20f0c703c/nvelope-1.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-09-23 09:55:02",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "monomonedula",
    "github_project": "nvelope",
    "travis_ci": true,
    "coveralls": false,
    "github_actions": false,
    "lcname": "nvelope"
}
        
Elapsed time: 2.64443s