Skip to content

Commit

Permalink
Merge pull request #9 from remigermain/#8-feat--empty-dict/list
Browse files Browse the repository at this point in the history
#8 feat  empty dict/list
  • Loading branch information
remigermain authored Feb 7, 2023
2 parents 1a92c49 + e68e987 commit faf346f
Show file tree
Hide file tree
Showing 15 changed files with 893 additions and 926 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
jobs:
build:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
runs-on: ubuntu-20.04

strategy:
matrix:
Expand All @@ -35,7 +35,6 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements/dev.txt ]; then pip install -r requirements/dev.txt; fi
- name: Lint with flake8
Expand All @@ -47,4 +46,5 @@ jobs:
- name: Test with pytest
run: |
pytest
python -m pytest -v -s --cov=nested_multipart_parser --cov-report=xml --capture=tee-sys ./tests
python -m coverage report -m
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ For this to work perfectly, you must follow the following rules:
- Don't put spaces between separators.

- 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)



Expand Down
4 changes: 1 addition & 3 deletions nested_multipart_parser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from .parser import NestedParser

__all__ = [
'NestedParser'
]
__all__ = ["NestedParser"]
73 changes: 73 additions & 0 deletions nested_multipart_parser/declare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
class NestedDeclare:
"""Create ditc/list wihout order"""

def __init__(self, _type=None, options=None):
self._elements = {}
self._options = options or {}
self.set_type(_type)

def __repr__(self):
return f"{type(self).__name__}({self._type.__name__})"

def set_type(self, _type):
self._type = _type
self._is_dict = _type is dict
self._is_list = _type is list
self._is_none = _type is None

def get_type(self):
return self._type

def set_type_from_key(self, key):
self.set_type(list if isinstance(key, int) else dict)

def conv_value(self, value):
if isinstance(value, type(self)):
value = value.convert()
return value

def __setitem__(self, key, value):
if self._is_none:
self.set_type_from_key(key)
if isinstance(key, int) and not self._is_list:
raise ValueError("int key cant be integer for dict object")
if not isinstance(key, int) and self._is_list:
raise ValueError("need integer key for list elements")

if key in self._elements:
if (
isinstance(value, type(self))
and isinstance(self._elements[key], type(self))
and self._elements[key].get_type() == value.get_type()
):
return

if self._options.get("raise_duplicate"):
raise ValueError("key is already set")

if not self._options.get("assign_duplicate"):
return

self._elements[key] = value

def __getitem__(self, key):
if key not in self._elements:
self[key] = type(self)(options=self._options)
return self._elements[key]

def _convert_list(self):
keys = sorted(self._elements.keys())
if keys != list(range(len(keys))):
raise ValueError("invalid format list keys")

return [self.conv_value(self._elements[key]) for key in keys]

def _convert_dict(self):
return {key: self.conv_value(value) for key, value in self._elements.items()}

def convert(self):
if self._is_none:
return None
if self._is_list:
return self._convert_list()
return self._convert_dict()
24 changes: 10 additions & 14 deletions nested_multipart_parser/drf.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,46 @@
from .parser import NestedParser as NestPars
from rest_framework.parsers import MultiPartParser, DataAndFiles
from rest_framework.parsers import MultiPartParser
from rest_framework.exceptions import ParseError
from django.http import QueryDict
from django.conf import settings

DRF_OPTIONS = {
"querydict": True
}
DRF_OPTIONS = {"querydict": True}

class NestedParser(NestPars):

class NestedParser(NestPars):
def __init__(self, data):
# merge django settings to default DRF_OPTIONS ( special parser options in on parser)
options = {
**DRF_OPTIONS,
**getattr(settings, "DRF_NESTED_MULTIPART_PARSER", {})
**getattr(settings, "DRF_NESTED_MULTIPART_PARSER", {}),
}
super().__init__(data, options)

def convert_value(self, data, key):
# all value in querydict as set in list
value = data[key]
if isinstance(value, list):
def convert_value(self, value):
if isinstance(value, list) and len(value) > 0:
return value[0]
return value

@property
def validate_data(self):
data = super().validate_data

# return dict ( not conver to querydict)
if not self._options["querydict"]:
return data

dtc = QueryDict(mutable=True)
dtc.update(data)
dtc.mutable = False
return dtc

class DrfNestedParser(MultiPartParser):

class DrfNestedParser(MultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
clsDataAndFile = super().parse(stream, media_type, parser_context)

data = clsDataAndFile.data.dict()
data.update(clsDataAndFile.files.dict()) # add files to data
data.update(clsDataAndFile.files.dict()) # add files to data

parser = NestedParser(data)
if parser.is_valid():
Expand Down
183 changes: 183 additions & 0 deletions nested_multipart_parser/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import re


class InvalidFormat(Exception):
"""key is invalid formated"""

def __init__(self, key):
super().__init__(f"invaid key format: {key}")


class NestedParserOptionsType(type):
def __new__(cls, cls_name, ns, childs):
if cls_name != "NestedParserOptionsAbstract" and cls_name:
if "sanitize" not in childs:
raise ValueError("you need to define sanitize methods")
return super().__new__(cls, cls_name, ns, childs)


TOKEN_PARSER = ("[", "]", ".")


class NestedParserOptionsAbstract(metaclass=NestedParserOptionsType):
def check(self, key, keys):
if len(keys) == 0:
raise InvalidFormat(key)

first = keys[0]
for token in TOKEN_PARSER:
if token in first:
raise InvalidFormat(key)

for key in keys:
if not isinstance(key, str):
continue
for c in key:
if c.isspace():
raise InvalidFormat(key)

def split(self, key):
contents = list(filter(None, self._reg_spliter.split(key)))
if not contents:
raise ValueError(f"invalid form key: {key}")

lst = [contents[0]]
if len(contents) >= 2:
lst.extend(self._reg_options.split(contents[1]))
if len(contents) == 3:
lst.append(contents[2])

return list(filter(None, lst))


REGEX_SEPARATOR = {
"dot": r"(\.[^\.]+)",
"bracket": r"([^\[\]]+)",
"mixed": r"(\[\d+\])|([^\[\]]+)",
"mixed-dot": r"(\[\d+\])|(\.[^\[\]\.]+)",
}


class NestedParserOptionsDot(NestedParserOptionsAbstract):
def __init__(self):
self._reg_spliter = re.compile(r"^([^\.]+)(.*?)(\.)?$")
self._reg_options = re.compile(r"(\.[^\.]+)")

def sanitize(self, key, value):
contents = self.split(key)
lst = contents[1:]
keys = [contents[0]]
for idx, k in enumerate(lst):
if k.startswith("."):
k = k[1:]
if not k:
if len(lst) != idx + 1:
raise InvalidFormat(key)
value = {}
break
try:
k = int(k)
except Exception:
pass
else:
raise InvalidFormat(key)
keys.append(k)

return keys, value


class NestedParserOptionsBracket(NestedParserOptionsAbstract):
def __init__(self):
self._reg_spliter = re.compile(r"^([^\[\]]+)(.*?)(\[\])?$")
self._reg_options = re.compile(r"(\[[^\[\]]+\])")

def sanitize(self, key, value):
first, *lst = self.split(key)
keys = [first]

for idx, k in enumerate(lst):
if k.startswith("[") or k.endswith("]"):
if not k.startswith("[") or not k.endswith("]"):
raise InvalidFormat(key)
k = k[1:-1]
if not k:
if len(lst) != idx + 1:
raise InvalidFormat(key)
value = []
break
try:
k = int(k)
except Exception:
pass
else:
raise InvalidFormat(key)
keys.append(k)
return keys, value


class NestedParserOptionsMixedDot(NestedParserOptionsAbstract):
def __init__(self):
self._reg_spliter = re.compile(r"^([^\[\]\.]+)(.*?)((?:\.)|(?:\[\]))?$")
self._reg_options = re.compile(r"(\[\d+\])|(\.[^\[\]\.]+)")

def sanitize(self, key, value):
first, *lst = self.split(key)
keys = [first]

for idx, k in enumerate(lst):
if k.startswith("."):
k = k[1:]
# empty dict
if not k:
if len(lst) != idx + 1:
raise InvalidFormat(key)
value = {}
break
elif k.startswith("[") or k.endswith("]"):
if not k.startswith("[") or not k.endswith("]"):
raise InvalidFormat(key)
k = k[1:-1]
if not k:
if len(lst) != idx + 1:
raise InvalidFormat(key)
value = []
break
k = int(k)
else:
raise InvalidFormat(key)
keys.append(k)

return keys, value


class NestedParserOptionsMixed(NestedParserOptionsMixedDot):
def __init__(self):
self._reg_spliter = re.compile(r"^([^\[\]\.]+)(.*?)((?:\.)|(?:\[\]))?$")
self._reg_options = re.compile(r"(\[\d+\])|(\.?[^\[\]\.]+)")

def sanitize(self, key, value):
first, *lst = self.split(key)
keys = [first]

for idx, k in enumerate(lst):
if k.startswith("."):
k = k[1:]
# empty dict
if not k:
if len(lst) != idx + 1:
raise InvalidFormat(key)
value = {}
break
elif k.startswith("[") or k.endswith("]"):
if not k.startswith("[") or not k.endswith("]"):
raise InvalidFormat(key)
k = k[1:-1]
if not k:
if len(lst) != idx + 1:
raise InvalidFormat(key)
value = []
break
k = int(k)
keys.append(k)

return keys, value
Loading

0 comments on commit faf346f

Please sign in to comment.