Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ on:
jobs:
build:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-20.04
# https://stackoverflow.com/questions/70959954/error-waiting-for-a-runner-to-pick-up-this-job-using-github-actions
runs-on: ubuntu-latest

strategy:
matrix:
python-version:
- "3.6"
- "3.9"
- "3.9"
- "3.11"
- "3.12"
- "3.13"

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5

- uses: actions/setup-python@v2
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- uses: actions/cache@v2
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements/dev.txt') }}
Expand Down
211 changes: 100 additions & 111 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
<a href="https://u8views.com/github/remigermain"><img src="https://u8views.com/api/v1/github/profiles/66946113/views/day-week-month-total-count.svg" width="1px" height="1px"></a>
[![CI](https://github.com/remigermain/nested-multipart-parser/actions/workflows/main.yml/badge.svg)](https://github.com/remigermain/nested-multipart-parser/actions/workflows/main.yml)
[![pypi](https://img.shields.io/pypi/v/nested-multipart-parser)](https://pypi.org/project/nested-multipart-parser/)
![PyPI - Downloads](https://img.shields.io/pypi/dm/Nested-multipart-parser)

Parser for nested data for '*multipart/form*', you can use it in any python project, or use the Django Rest Framework integration.
[![PyPI - Downloads](https://img.shields.io/pypi/dm/Nested-multipart-parser)](https://pypistats.org/packages/nested-multipart-parser)

Parser for nested data for *multipart/form*, usable in any Python project or via the [Django Rest Framework integration](https://www.django-rest-framework.org/community/third-party-packages/#parsers)..
# Installation:

```bash
Expand Down Expand Up @@ -35,6 +34,16 @@ def my_view():

### Django Rest Framework

you can define parser for all view in settings.py
```python
REST_FRAMEWORK = {
"DEFAULT_PARSER_CLASSES": [
"nested_multipart_parser.drf.DrfNestedParser",
]
}
```
or directly in your view

```python
from nested_multipart_parser.drf import DrfNestedParser
...
Expand All @@ -46,7 +55,7 @@ class YourViewSet(viewsets.ViewSet):

## What it does:

The parser take the request data and transform it to a Python dictionary:
The parser takes the request data and transforms it into a Python dictionary.

example:

Expand Down Expand Up @@ -94,126 +103,106 @@ example:
}
```

## How it works:

Attributes where sub keys are full numbers only are automatically converted into lists:
## How it works
### Lists

Attributes whose sub‑keys are *only numbers* become Python lists:
```python
data = {
'title[0]': 'my-value',
'title[1]': 'my-second-value'
}
output = {
'title': [
'my-value',
'my-second-value'
]
}

# Be aware of the fact that you have to respect the order of the indices for arrays, thus
'title[2]': 'my-value' # Invalid (you have to set title[0] and title[1] before)

# Also, you can't create an array on a key already set as a prinitive value (int, boolean or string):
'title': 42,
'title[object]': 42 # Invalid
data = {
'title[0]': 'my-value',
'title[1]': 'my-second-value'
}
output = {
'title': [
'my-value',
'my-second-value'
]
}
```
> Important notes



Attributes where sub keys are other than full numbers are converted into Python dictionary:

- Indices must be contiguous and start at 0.
- You cannot turn a primitive (int, bool, str) into a list later, e.g.
```python
data = {
'title.key0': 'my-value',
'title.key7': 'my-second-value'
}
output = {
'title': {
'key0': 'my-value',
'key7': 'my-second-value'
}
}


# You have no limit for chained key:
# with "mixed-dot" separator option (same as 'mixed' but with dot after list to object):
data = {
'the[0].chained.key[0].are.awesome[0][0]': 'im here !!'
}
# with "mixed" separator option:
data = {
'the[0]chained.key[0]are.awesome[0][0]': 'im here !!'
}
# With "bracket" separator option:
data = {
'the[0][chained][key][0][are][awesome][0][0]': 'im here !!'
}
# With "dot" separator option:
data = {
'the.0.chained.key.0.are.awesome.0.0': 'im here !!'
}
'title': 42,
'title[object]': 42 # ❌ invalid
```

### Dictionaries

Attributes whose sub‑keys are *not pure numbers* become nested dictionaries:
```python
data = {
'title.key0': 'my-value',
'title.key7': 'my-second-value'
}
output = {
'title': {
'key0': 'my-value',
'key7': 'my-second-value'
}
}
```

For this to work perfectly, you must follow the following rules:

- A first key always need to be set. ex: `title[0]` or `title`. In both cases the first key is `title`

- For `mixed` or `mixed-dot` options, brackets `[]` is for list, and dot `.` is for object
### Chaining keys

- For `mixed-dot` options is look like `mixed` but with dot when object follow list
>Keys can be chained arbitrarily. Below are examples for each separator option:

- For `bracket` each sub key need to be separate by brackets `[ ]` or with `dot` options `.`
|Separator| Example key | Meaning|
|-|-|-|
|mixed‑dot| the[0].chained.key[0].are.awesome[0][0] |List → object → list → object …|
|mixed| the[0]chained.key[0]are.awesome[0][0] | Same as mixed‑dot but without the dot after a list|
|bracket| the[0][chained][key][0][are][awesome][0][0] | Every sub‑key is wrapped in brackets|
|dot |the.0.chained.key.0.are.awesome.0.0 | Dots separate every level; numeric parts become lists|

- For `bracket` or `dot`options, if a key is number is convert to list else a object

- Don't put spaces between separators.
Rules to keep in mind
- First key must exist – e.g. title[0] or just title.
- For mixed / mixed‑dot, [] denotes a list and . denotes an object.
- mixed‑dot behaves like mixed but inserts a dot when an object follows a list.
- For bracket, each sub‑key must be surrounded by brackets ([ ]).
- For bracket or dot, numeric sub‑keys become list elements; non‑numeric become objects.
- No spaces between separators.
- By default, duplicate keys are disallowed (see options).
- Empty structures are supported:
Empty list → "article.authors[]": None → {"article": {"authors": []}}
Empty dict → "article.": None → {"article": {}} (available with dot, mixed, mixed‑dot)

- By default, you can't set set duplicates keys (see options)

- You can set empty dict/list:
for empty list: `"article.authors[]": None` -> `{"article": {"authors": [] }}`
for empty dict: `"article.": None` -> `{"article": {} }`
`.` last dot for empty dict (availables in `dot`, `mixed` and `mixed-dot` options)
`[]` brackets empty for empty list (availables in `brackets`, `mixed` and `mixed-dot` options)



## Options

```python
{
# Separators:
# with mixed-dot: article[0].title.authors[0]: "jhon doe"
# with mixed: article[0]title.authors[0]: "jhon doe"
# with bracket: article[0][title][authors][0]: "jhon doe"
# with dot: article.0.title.authors.0: "jhon doe"
'separator': 'bracket' or 'dot' or 'mixed' or 'mixed-dot', # default is `mixed-dot`


# raise a expections when you have duplicate keys
# ex :
# {
# "article": 42,
# "article[title]": 42,
# }
'raise_duplicate': True, # default is True

# override the duplicate keys, you need to set "raise_duplicate" to False
# ex :
# {
# "article": 42,
# "article[title]": 42,
# }
# the out is
# ex :
# {
# "article"{
# "title": 42,
# }
# }
'assign_duplicate': False # default is False
# Separator (default: 'mixed‑dot')
# mixed‑dot : article[0].title.authors[0] -> "john doe"
# mixed : article[0]title.authors[0] -> "john doe"
# bracket : article[0][title][authors][0] -> "john doe"
# dot : article.0.title.authors.0 -> "john doe"
'separator': 'bracket' | 'dot' | 'mixed' | 'mixed‑dot',

# Raise an exception when duplicate keys are encountered
# Example:
# {
# "article": 42,
# "article[title]": 42,
# }
'raise_duplicate': True, # default: True

# Override duplicate keys (requires raise_duplicate=False)
# Example:
# {
# "article": 42,
# "article[title]": 42,
# }
# Result:
# {
# "article": {
# "title": 42
# }
# }
'assign_duplicate': False, # default: False
}
```

Expand All @@ -223,20 +212,20 @@ For this to work perfectly, you must follow the following rules:
# settings.py
...

# settings.py
DRF_NESTED_MULTIPART_PARSER = {
"separator": "mixed-dot",
"raise_duplicate": True,
"assign_duplicate": False,
"separator": "mixeddot",
"raise_duplicate": True,
"assign_duplicate": False,

# output of parser is converted to querydict
# if is set to False, dict python is returned
"querydict": True,
# If True, the parser’s output is converted to a QueryDict;
# if False, a plain Python dict is returned.
"querydict": True,
}
```

## JavaScript integration:

You can use this [multipart-object](https://github.com/remigermain/multipart-object) library to easy convert object to flat nested object formatted for this library
A companion [multipart-object](https://github.com/remigermain/multipart-object) library exists to convert a JavaScript object into the flat, nested format expected by this parser.

## License

Expand Down
77 changes: 77 additions & 0 deletions bench/bench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import time

from nested_multipart_parser import NestedParser


def bench(data, count):
v = []
for _ in range(count):
start = time.perf_counter()
parser = NestedParser(data)
parser.is_valid()
validate_data = parser.validate_data
end = time.perf_counter()
v.append(end - start)

return sum(v) / len(v)


def big(count):
data = {
"title": "title",
"date": "time",
"langs[0].id": "id",
"langs[0].title": "title",
"langs[0].description": "description",
"langs[0].language": "language",
"langs[1].id": "id1",
"langs[1].title": "title1",
"langs[1].description": "description1",
"langs[1].language": "language1",
"test.langs[0].id": "id",
"test.langs[0].title": "title",
"test.langs[0].description": "description",
"test.langs[0].language": "language",
"test.langs[1].id": "id1",
"test.langs[1].title": "title1",
"test.langs[1].description": "description1",
"test.langs[1].language": "language1",
"deep.nested.dict.test.langs[0].id": "id",
"deep.nested.dict.test.langs[0].title": "title",
"deep.nested.dict.test.langs[0].description": "description",
"deep.nested.dict.test.langs[0].language": "language",
"deep.nested.dict.test.langs[1].id": "id1",
"deep.nested.dict.test.langs[1].title": "title1",
"deep.nested.dict.test.langs[1].description": "description1",
"deep.nested.dict.test.langs[1].language": "language1",
"deep.nested.dict.with.list[0].test.langs[0].id": "id",
"deep.nested.dict.with.list[0].test.langs[0].title": "title",
"deep.nested.dict.with.list[1].test.langs[0].description": "description",
"deep.nested.dict.with.list[1].test.langs[0].language": "language",
"deep.nested.dict.with.list[1].test.langs[1].id": "id1",
"deep.nested.dict.with.list[1].test.langs[1].title": "title1",
"deep.nested.dict.with.list[0].test.langs[1].description": "description1",
"deep.nested.dict.with.list[0].test.langs[1].language": "language1",
}
return bench(data, count)


def small(count):
data = {
"title": "title",
"date": "time",
"langs[0].id": "id",
"langs[0].title": "title",
"langs[0].description": "description",
"langs[0].language": "language",
"langs[1].id": "id1",
"langs[1].title": "title1",
"langs[1].description": "description1",
"langs[1].language": "language1",
}
return bench(data, count)


count = 10_000
print(f"{small(count)=}")
print(f"{big(count)=}")
Loading