Skip to content

Commit

Permalink
Merge pull request #46 from Ousret/develop
Browse files Browse the repository at this point in the history
Prepare release 2.3.0
  • Loading branch information
Ousret authored Feb 2, 2021
2 parents 411c38d + dca7b6a commit 47b1356
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 10 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
dist: xenial
dist: focal
language: python
cache: pip

python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10-dev"
- "pypy3"

jobs:
allow_failures:
- python: "3.5"
- python: "3.10-dev"
- python: "pypy3"

before_install:
Expand Down
132 changes: 130 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@

### ❓ Why

No matter if you are currently building software using HTTP or IMAP _(message, email)_, you should not worry about easily accessing header and associated attributes, adjectives or values.
No matter if you are currently creating/using code using HTTP or IMAP _(message, email)_, you should not worry about easily accessing header and associated attributes, adjectives or values.

<p align="center">
<img src="https://user-images.githubusercontent.com/9326700/77257881-55866300-6c77-11ea-820c-7550e6bdeee7.gif" alt="using kiss-headers from python interpreter"/>
</p>

I have seen so many chunks of code trying to deal with these headers; often I saw this implementation:
```python
# No more of that!
charset = headers['Content-Type'].split(';')[-1].split('=')[-1].replace('"', '')
```
**No more of that!**

**Scroll down and see how you'll do it from now on.**

## 🔪 Features

Expand All @@ -50,6 +52,7 @@ charset = headers['Content-Type'].split(';')[-1].split('=')[-1].replace('"', '')
* Capability to alter headers using simple, human-readable operator notation `+` and `-`.
* Flexibility if headers are from an email or HTTP, use as you need with one library.
* Ability to parse any object and extract recognized headers from it, it also supports UTF-8 encoded headers.
* Offer an opinionated way to un/serialize headers.
* Fully type-annotated.
* Provide great auto-completion in Python interpreter or any capable IDE.
* No dependencies. And never will be.
Expand Down Expand Up @@ -136,6 +139,131 @@ headers.from_ # to access From, just add a single underscore to it
headers['from']
```

#### ✍️Serialization

Since version 2.3.0 the package offer the possibility to un/serialize `Headers`.

```python
from requests import get
from kiss_headers import parse_it, dumps

json_repr: str = dumps(
parse_it(
get("https://www.google.fr")
),
indent=4
)

print(json_repr) # See the result bellow

# Additionally, how to parse the JSON repr to Headers again
headers = parse_it(json_repr) # Yes! that easy!
```

```json
{
"Date": [
{
"Tue, 02 Feb 2021 21:43:13 GMT": null
}
],
"Expires": [
{
"-1": null
}
],
"Cache-Control": [
{
"private": null
},
{
"max-age": "0"
}
],
"Content-Type": [
{
"text/html": null,
"charset": "ISO-8859-1"
}
],
"P3P": [
{
"CP": "This is not a P3P policy! See g.co/p3phelp for more info."
}
],
"Content-Encoding": [
{
"gzip": null
}
],
"Server": [
{
"gws": null
}
],
"X-XSS-Protection": [
{
"0": null
}
],
"X-Frame-Options": [
{
"SAMEORIGIN": null
}
],
"Set-Cookie": [
{
"NID": "208=D5XUqjrP9PNpiZu4laa_0xvy_IxBzQLtfxqeAqcPBgiY2y5sfSF51IFuXZnH0zDAF1KZ8x-0VsRyGOM0aStIzCUfdiPBOCxHSxUv39N0vwzku3aI2UkeRXhWw8-HWw5Ob41GB0PZi2coQsPM7ZEQ_fl9PlQ_ld1KrPA",
"expires": "Wed, 04-Aug-2021 21:43:13 GMT",
"path": "/",
"domain": ".google.fr",
"HttpOnly": null
},
{
"CONSENT": "PENDING+880",
"expires": "Fri, 01-Jan-2038 00:00:00 GMT",
"path": "/",
"domain": ".google.fr"
}
],
"Alt-Svc": [
{
"h3-29": ":443",
"ma": "2592000"
},
{
"h3-T051": ":443",
"ma": "2592000"
},
{
"h3-Q050": ":443",
"ma": "2592000"
},
{
"h3-Q046": ":443",
"ma": "2592000"
},
{
"h3-Q043": ":443",
"ma": "2592000"
},
{
"quic": ":443",
"ma": "2592000",
"v": "46,43"
}
],
"Transfer-Encoding": [
{
"chunked": null
}
]
}
```

Alternatively you may use `from kiss_headers import parse_it, encode, decode` to transform `Headers` to `dict` (instead of JSON) or the other way around.
Understand that the `dict` returned in `encode` will differ from the method `to_dict()` in `Headers`.

#### 🛠️ Create headers from objects

Introduced in the version 2.0, kiss-headers now allow you to create headers with more than 40+ ready-to-use, fully documented, header objects.
Expand Down
3 changes: 2 additions & 1 deletion kiss_headers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
:license: MIT, see LICENSE for more details.
"""

from kiss_headers.api import explain, get_polymorphic, parse_it
from kiss_headers.api import dumps, explain, get_polymorphic, parse_it
from kiss_headers.builder import (
Accept,
AcceptEncoding,
Expand Down Expand Up @@ -88,4 +88,5 @@
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__
11 changes: 10 additions & 1 deletion kiss_headers/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from email.message import Message
from email.parser import HeaderParser
from io import BufferedReader, RawIOBase
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 (
class_to_header_name,
Expand All @@ -22,14 +24,17 @@
def parse_it(raw_headers: Any) -> Headers:
"""
Just decode anything that could contain headers. That simple PERIOD.
:param raw_headers: Accept bytes, str, fp, dict, email.Message, requests.Response, urllib3.HTTPResponse and httpx.Response.
:param raw_headers: Accept bytes, str, fp, dict, JSON, email.Message, requests.Response, urllib3.HTTPResponse and httpx.Response.
:raises:
TypeError: If passed argument cannot be parsed to extract headers from it.
"""

headers: Optional[Iterable[Tuple[str, Any]]] = None

if isinstance(raw_headers, str):
if raw_headers.startswith("{") and raw_headers.endswith("}"):
return decode(json_loads(raw_headers))

headers = HeaderParser().parsestr(raw_headers, headersonly=True).items()
elif (
isinstance(raw_headers, bytes)
Expand Down Expand Up @@ -175,3 +180,7 @@ def get_polymorphic(
header.__class__ = desired_output

return r # type: ignore


def dumps(headers: Headers, **kwargs: Optional[Any]) -> str:
return json_dumps(encode(headers), **kwargs) # type: ignore
2 changes: 1 addition & 1 deletion kiss_headers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def insert(
'Content-Type: application/json; charset="UTF-8"; format="flowed"'
"""

__index = __index if __index >= 0 else __index % len(self._attrs)
__index = __index if __index >= 0 else __index % (len(self._attrs) or 1)

for member in __members:
self._attrs.insert(member, None, __index)
Expand Down
66 changes: 66 additions & 0 deletions kiss_headers/serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from typing import Dict, List, Optional, Union

from kiss_headers.models import Header, Headers


def encode(headers: Headers) -> Dict[str, List[Dict]]:
"""
Provide an opinionated but reliable way to encode headers to dict for serialization purposes.
"""
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

if isinstance(encoded_header[attribute], list) is False:
# Here encoded_header[attribute] most certainly is str
# Had to silent mypy error.
encoded_header[attribute] = [encoded_header[attribute]] # type: ignore

encoded_header[attribute].append(value) # type: ignore

result[header.name].append(encoded_header)

return result


def decode(encoded_headers: Dict[str, List[Dict]]) -> Headers:
"""
Decode any previously encoded headers to a Headers object.
"""
headers: Headers = Headers()

for header_name, encoded_header_list in encoded_headers.items():
if not isinstance(encoded_header_list, list):
raise ValueError("Decode require first level values to be List")

for encoded_header in encoded_header_list:
if not isinstance(encoded_header, dict):
raise ValueError("Decode require each list element to be Dict")

header = Header(header_name, "")

for attr, value in encoded_header.items():
if value is None:
header += attr
continue
if isinstance(value, str):
header[attr] = value
continue

for sub_value in value:
header.insert(-1, **{attr: sub_value})

headers += header

return headers
2 changes: 1 addition & 1 deletion kiss_headers/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
Expose version
"""

__version__ = "2.2.4"
__version__ = "2.3.0"
VERSION = __version__.split(".")
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_version():

# Package meta-data.
NAME = "kiss-headers"
DESCRIPTION = "Python package for object oriented headers, HTTP/1.1 style. Also parse headers."
DESCRIPTION = "Python package for object oriented headers, HTTP/1.1 style. Parser and serializer for http headers."
URL = "https://github.com/ousret/kiss-headers"
EMAIL = "[email protected]"
AUTHOR = "Ahmed TAHRI @Ousret"
Expand Down Expand Up @@ -71,6 +71,7 @@ def get_version():
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Utilities",
"Programming Language :: Python :: Implementation :: PyPy",
Expand Down
Loading

0 comments on commit 47b1356

Please sign in to comment.