Skip to content

Commit

Permalink
🔖 Release 2.4.0 (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ousret authored Sep 30, 2023
1 parent ca39a55 commit 6c2fd5d
Show file tree
Hide file tree
Showing 31 changed files with 188 additions and 192 deletions.
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Pull Request

### My patch is about
- [ ] :bug: Bugfix
- [ ] :bug: Bugfix
- [ ] :arrow_up: Improvement
- [ ] :pencil: Documentation
- [ ] :heavy_check_mark: Tests
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: 🎨 Linters

on: [push, pull_request]
on:
push:
branches:
- master
- main
pull_request:

jobs:
lint:
Expand All @@ -20,11 +25,11 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -U pip setuptools
pip install -U pip build wheel
pip install -r dev-requirements.txt
- name: Install the package
run: |
python setup.py install
pip install .
- name: Type checking (Mypy)
run: |
mypy kiss_headers
Expand All @@ -33,4 +38,4 @@ jobs:
isort --check kiss_headers
- name: Code format (Black)
run: |
black --check --diff --target-version=py36 kiss_headers
black --check --diff --target-version=py37 kiss_headers
6 changes: 3 additions & 3 deletions .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
pip install build wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
python -m build
twine upload dist/*
13 changes: 9 additions & 4 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: Tests

on: [push, pull_request]
on:
push:
branches:
- main
- master
pull_request:

jobs:
tests:
Expand All @@ -9,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"]
os: [ubuntu-latest]

steps:
Expand All @@ -20,11 +25,11 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -U pip setuptools
pip install -U pip build wheel
pip install -r dev-requirements.txt
- name: Install the package
run: |
python setup.py install
pip install .
- name: Run tests
run: |
pytest
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ response = get(
* Fully type-annotated.
* Provide great auto-completion in Python interpreter or any capable IDE.
* No dependencies. Never will be.
* Support JSON in header value.
* 90% test coverage.

Plus all the features that you would expect from handling headers...
Expand All @@ -67,7 +68,7 @@ Plus all the features that you would expect from handling headers...

### ✨ Installation

Whatever you like, use `pipenv` or `pip`, it simply works. Requires Python 3.6+ installed.
Whatever you like, use `pipenv` or `pip`, it simply works. Requires Python 3.7+ installed.
```sh
pip install kiss-headers --upgrade
```
Expand Down
10 changes: 5 additions & 5 deletions kiss_headers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
:license: MIT, see LICENSE for more details.
"""

from kiss_headers.api import dumps, explain, get_polymorphic, parse_it
from kiss_headers.builder import (
from .api import dumps, explain, get_polymorphic, parse_it
from .builder import (
Accept,
AcceptEncoding,
AcceptLanguage,
Expand Down Expand Up @@ -87,9 +87,9 @@
XFrameOptions,
XXssProtection,
)
from kiss_headers.models import Attributes, Header, Headers, lock_output_type
from kiss_headers.serializer import decode, encode
from kiss_headers.version import VERSION, __version__
from .models import Attributes, Header, Headers, lock_output_type
from .serializer import decode, encode
from .version import VERSION, __version__

__all__ = (
"dumps",
Expand Down
21 changes: 14 additions & 7 deletions kiss_headers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@
from json import dumps as json_dumps, loads as json_loads
from typing import Any, Iterable, List, Mapping, Optional, Tuple, Type, TypeVar, Union

from kiss_headers.models import Header, Headers
from kiss_headers.serializer import decode, encode
from kiss_headers.structures import CaseInsensitiveDict
from kiss_headers.utils import (
from .builder import CustomHeader
from .models import Header, Headers
from .serializer import decode, encode
from .structures import CaseInsensitiveDict
from .utils import (
class_to_header_name,
decode_partials,
extract_class_name,
extract_encoded_headers,
header_content_split,
header_name_to_class,
is_content_json_object,
is_legal_header_name,
normalize_str,
)

T = TypeVar("T")
T = TypeVar("T", bound=CustomHeader, covariant=True)


def parse_it(raw_headers: Any) -> Headers:
Expand Down Expand Up @@ -88,12 +90,17 @@ def parse_it(raw_headers: Any) -> Headers:
list_of_headers: List[Header] = []

for head, content in revised_headers:

# We should ignore when a illegal name is considered as an header. We avoid ValueError (in __init__ of Header)
if is_legal_header_name(head) is False:
continue

entries: List[str] = header_content_split(content, ",")
is_json_obj: bool = is_content_json_object(content)
entries: List[str]

if is_json_obj is False:
entries = header_content_split(content, ",")
else:
entries = [content]

# Multiple entries are detected in one content at the only exception that its not IMAP header "Subject".
if len(entries) > 1 and normalize_str(head) != "subject":
Expand Down
4 changes: 2 additions & 2 deletions kiss_headers/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from typing import Dict, List, Optional, Tuple, Union
from urllib.parse import quote as url_quote, unquote as url_unquote

from kiss_headers.models import Header
from kiss_headers.utils import (
from .models import Header
from .utils import (
class_to_header_name,
header_content_split,
prettify_header_name,
Expand Down
45 changes: 29 additions & 16 deletions kiss_headers/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from copy import deepcopy
from json import dumps
from json import JSONDecodeError, dumps, loads
from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union

from kiss_headers.structures import AttributeBag, CaseInsensitiveDict
from kiss_headers.utils import (
from .structures import AttributeBag, CaseInsensitiveDict
from .utils import (
escape_double_quote,
extract_comments,
header_content_split,
header_name_to_class,
is_content_json_object,
is_legal_header_name,
normalize_list,
normalize_str,
Expand Down Expand Up @@ -54,7 +55,29 @@ def __init__(self, name: str, content: str):
self._pretty_name: str = prettify_header_name(self._name)
self._content: str = content

self._members: List[str] = header_content_split(self._content, ";")
self._members: List[str]

if is_content_json_object(self._content):
try:
payload = loads(self._content)
except JSONDecodeError as e:
raise ValueError(
f"Header '{self._name}' contain an invalid JSON value."
) from e

if isinstance(payload, list):
self._members = payload
elif isinstance(payload, dict):
self._members = []
for k, v in payload.items():
if v is not None:
self._members.append(f"{str(k)}={str(v)}")
else:
self._members.append(str(k))
else:
raise ValueError(f"Header '{self._name}' is malformed.")
else:
self._members = header_content_split(self._content, ";")

self._attrs: Attributes = Attributes(self._members)

Expand Down Expand Up @@ -264,7 +287,6 @@ def __add__(self, other: Union[str, "Header"]) -> Union["Header", "Headers"]:
)

if isinstance(other, Header):

headers = Headers()
headers += self
headers += other
Expand Down Expand Up @@ -644,6 +666,8 @@ class Headers(object):

from_: Union[Header, List[Header]]

report_to: Union[Header, List[Header]]

def __init__(self, *headers: Union[List[Header], Header]):
"""
:param headers: Initial list of header. Can be empty.
Expand Down Expand Up @@ -801,11 +825,9 @@ def __setitem__(self, key: str, value: str) -> None:

# Permit to detect multiple entries.
if normalize_str(key) != "subject":

entries: List[str] = header_content_split(value, ",")

if len(entries) > 1:

for entry in entries:
self._headers.append(Header(key, entry))

Expand Down Expand Up @@ -878,7 +900,6 @@ def __repr__(self) -> str:
result: List[str] = []

for header_name in self.keys():

r = self.get(header_name)

if not r:
Expand Down Expand Up @@ -1193,7 +1214,6 @@ def __init__(self, members: List[str]):
self._bag: AttributeBag = CaseInsensitiveDict()

for member, index in zip(members, range(0, len(members))):

if member == "":
continue

Expand Down Expand Up @@ -1255,19 +1275,16 @@ def __eq__(self, other: object) -> bool:
list_check: List[Tuple[int, str, Optional[str]]] = []

for index_a, key_a, value_a in list_repr_a:

key_a = normalize_str(key_a)

for index_b, key_b, value_b in list_repr_b:

key_b = normalize_str(key_b)

if (
key_a == key_b
and value_a == value_b
and (index_a, key_a, key_b) not in list_check
):

list_check.append((index_a, key_a, key_b))

return len(list_check) == len(list_repr_a)
Expand Down Expand Up @@ -1358,7 +1375,6 @@ def remove(
freed_indexes: List[int] = []

if index is not None:

if with_value is not None:
raise ValueError(
"Cannot set both index and with_value in the remove method."
Expand All @@ -1373,9 +1389,7 @@ def remove(
freed_indexes.append(self._bag[key][1].pop(pos))

if index is None:

if with_value is not None:

for index_, value_ in zip(self._bag[key][1], self._bag[key][0]):
if with_value is True and value_ is not None:
freed_indexes.append(index_)
Expand All @@ -1397,7 +1411,6 @@ def remove(
del self._bag[key]

for attr in self._bag:

values, indexes = self._bag[attr]
max_freed_index: int = max(freed_indexes)

Expand Down
4 changes: 1 addition & 3 deletions kiss_headers/serializer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Dict, List, Optional, Union

from kiss_headers.models import Header, Headers
from .models import Header, Headers


def encode(headers: Headers) -> Dict[str, List[Dict]]:
Expand All @@ -10,14 +10,12 @@ def encode(headers: Headers) -> Dict[str, List[Dict]]:
result: Dict[str, List[Dict]] = dict()

for header in headers:

if header.name not in result:
result[header.name] = list()

encoded_header: Dict[str, Union[Optional[str], List[str]]] = dict()

for attribute, value in header:

if attribute not in encoded_header:
encoded_header[attribute] = value
continue
Expand Down
14 changes: 11 additions & 3 deletions kiss_headers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ def header_content_split(string: str, delimiter: str) -> List[str]:
result: List[str] = [""]

for letter, index in zip(string, range(0, len(string))):

if letter == '"':
in_double_quote = not in_double_quote

Expand All @@ -126,7 +125,6 @@ def header_content_split(string: str, delimiter: str) -> List[str]:
}

if not in_double_quote:

if not in_value and letter == "=":
in_value = True
elif letter == ";" and in_value:
Expand All @@ -138,7 +136,6 @@ def header_content_split(string: str, delimiter: str) -> List[str]:
if letter == delimiter and (
(in_value or in_double_quote or in_parenthesis or is_on_a_day) is False
):

result[-1] = result[-1].lstrip().rstrip()
result.append("")

Expand Down Expand Up @@ -453,3 +450,14 @@ def escape_double_quote(content: str) -> str:
'UTF\\"-8'
"""
return unescape_double_quote(content).replace('"', r"\"")


def is_content_json_object(content: str) -> bool:
"""
Sometime, you may receive a header that hold a JSON list or object.
This function detect it.
"""
content = content.strip()
return (content.startswith("{") and content.endswith("}")) or (
content.startswith("[") and content.endswith("]")
)
Loading

0 comments on commit 6c2fd5d

Please sign in to comment.