diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7f84847..3878f41 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: ["3.10"] os: [ubuntu-latest] steps: @@ -38,4 +38,4 @@ jobs: isort --check kiss_headers - name: Code format (Black) run: | - black --check --diff --target-version=py37 kiss_headers + black --check --diff kiss_headers diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..cc49679 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,86 @@ +name: Publish to PyPI + +on: + release: + types: + - created + +permissions: + contents: read + +jobs: + build: + name: "Build dists" + runs-on: "ubuntu-latest" + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + + steps: + - name: "Checkout repository" + uses: "actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608" + + - name: "Setup Python" + uses: "actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1" + with: + python-version: "3.x" + + - name: "Install dependencies" + run: python -m pip install build + + - name: "Build dists" + run: | + SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \ + python -m build + + - name: "Generate hashes" + id: hash + run: | + cd dist && echo "::set-output name=hashes::$(sha256sum * | base64 -w0)" + + - name: "Upload dists" + uses: "actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce" + with: + name: "dist" + path: "dist/" + if-no-files-found: error + retention-days: 5 + + provenance: + needs: [build] + permissions: + actions: read + contents: write + id-token: write # Needed to access the workflow's OIDC identity. + uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0" + with: + base64-subjects: "${{ needs.build.outputs.hashes }}" + upload-assets: true + compile-generator: true # Workaround for https://github.com/slsa-framework/slsa-github-generator/issues/1163 + + publish: + name: "Publish" + if: startsWith(github.ref, 'refs/tags/') + environment: + name: pypi + url: https://pypi.org/p/kiss-headers + needs: ["build", "provenance"] + permissions: + contents: write + id-token: write # Needed for trusted publishing to PyPI. + runs-on: "ubuntu-latest" + + steps: + - name: "Download dists" + uses: "actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a" + with: + name: "dist" + path: "dist/" + + - name: "Upload dists to GitHub Release" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + run: | + gh release upload ${{ github.ref_name }} dist/* --repo ${{ github.repository }} + + - name: "Publish dists to PyPI" + uses: "pypa/gh-action-pypi-publish@f8c70e705ffc13c3b4d1221169b84f12a75d6ca8" # v1.8.8 diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml deleted file mode 100644 index bf394f5..0000000 --- a/.github/workflows/pythonpublish.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -name: Upload Python Package - -on: - release: - types: [created] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build wheel twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python -m build - twine upload dist/* diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index deff995..4723812 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] os: [ubuntu-latest] steps: diff --git a/README.md b/README.md index e7e19b5..a64d786 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Plus all the features that you would expect from handling headers... * Properties syntax for headers and attribute in a header. * Supports headers and attributes OneToOne, OneToMany and ManySquashedIntoOne. -* Capable of parsing `bytes`, `fp`, `str`, `dict`, `email.Message`, `requests.Response`, `httpx._models.Response` and `urllib3.HTTPResponse`. +* Capable of parsing `bytes`, `fp`, `str`, `dict`, `email.Message`, `requests.Response`, `niquests.Response`, `httpx._models.Response` and `urllib3.HTTPResponse`. * Automatically unquote and unfold the value of an attribute when retrieving it. * Keep headers and attributes ordering. * Case-insensitive with header name and attribute key. @@ -73,6 +73,8 @@ Whatever you like, use `pipenv` or `pip`, it simply works. Requires Python 3.7+ pip install kiss-headers --upgrade ``` +This project is included in [Niquests](https://github.com/jawah/niquests)! Your awesome drop-in replacement for Requests! + ### 🍰 Usage #### Quick start diff --git a/kiss_headers/api.py b/kiss_headers/api.py index a521fe2..b98416c 100644 --- a/kiss_headers/api.py +++ b/kiss_headers/api.py @@ -1,3 +1,4 @@ +from copy import deepcopy from email.message import Message from email.parser import HeaderParser from io import BufferedReader, RawIOBase @@ -27,17 +28,20 @@ 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, JSON, email.Message, requests.Response, urllib3.HTTPResponse and httpx.Response. + If passed with a Headers instance, return a deep copy of it. + :param raw_headers: Accept bytes, str, fp, dict, JSON, email.Message, requests.Response, niquests.Response, urllib3.HTTPResponse and httpx.Response. :raises: TypeError: If passed argument cannot be parsed to extract headers from it. """ + if isinstance(raw_headers, Headers): + return deepcopy(raw_headers) + headers: Optional[Iterable[Tuple[Union[str, bytes], Union[str, bytes]]]] = 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) @@ -54,7 +58,7 @@ def parse_it(raw_headers: Any) -> Headers: r = extract_class_name(type(raw_headers)) if r: - if r == "requests.models.Response": + if r in ["requests.models.Response", "niquests.models.Response"]: headers = [] for header_name in raw_headers.raw.headers: for header_content in raw_headers.raw.headers.getlist(header_name): diff --git a/kiss_headers/models.py b/kiss_headers/models.py index 1e2f556..8600b0c 100644 --- a/kiss_headers/models.py +++ b/kiss_headers/models.py @@ -22,7 +22,7 @@ OUTPUT_LOCK_TYPE: bool = False -class Header(object): +class Header: """ Object representation of a single Header. """ @@ -606,7 +606,7 @@ def __contains__(self, item: str) -> bool: return False -class Headers(object): +class Headers: """ Object-oriented representation for Headers. Contains a list of Header with some level of abstraction. Combine advantages of dict, CaseInsensibleDict, list, multi-dict, and native objects. @@ -1211,7 +1211,7 @@ def __dir__(self) -> Iterable[str]: ) -class Attributes(object): +class Attributes: """ Dedicated class to handle attributes within a Header. Wrap an AttributeBag and offer methods to manipulate it with ease. diff --git a/kiss_headers/version.py b/kiss_headers/version.py index f2b4cfb..fa33b69 100644 --- a/kiss_headers/version.py +++ b/kiss_headers/version.py @@ -2,5 +2,5 @@ Expose version """ -__version__ = "2.4.2" +__version__ = "2.4.3" VERSION = __version__.split(".") diff --git a/tests/test_headers_from_string.py b/tests/test_headers_from_string.py index 9fa33ec..d7778b7 100644 --- a/tests/test_headers_from_string.py +++ b/tests/test_headers_from_string.py @@ -39,6 +39,39 @@ "\n", "\r\n" ) +RAW_HEADERS_WITH_CONNECT = """HTTP/1.1 200 Connection established + +HTTP/2 200 +date: Tue, 28 Sep 2021 13:45:34 GMT +content-type: application/epub+zip +content-length: 3706401 +content-disposition: filename=ipython-readthedocs-io-en-stable.epub +x-amz-id-2: 2PO2WHP4qGqkhyC1VbRE2KLN2g4uk38vYzaNJDU/OBSxh4lUtYgERD2FNAOPkKPD1a6rsNBMeKI= +x-amz-request-id: 21E21R71FAY4WQKT +last-modified: Sat, 25 Sep 2021 00:43:37 GMT +etag: "6f512f04591f7667486d044c54708448" +x-served: Nginx-Proxito-Sendfile +x-backend: web-i-078619706c1392c2c +x-rtd-project: ipython +x-rtd-version: stable +x-rtd-path: /proxito/epub/ipython/stable/ipython.epub +x-rtd-domain: ipython.readthedocs.io +x-rtd-version-method: path +x-rtd-project-method: subdomain +referrer-policy: no-referrer-when-downgrade +permissions-policy: interest-cohort=() +strict-transport-security: max-age=31536000; includeSubDomains; preload +cf-cache-status: HIT +age: 270 +expires: Tue, 28 Sep 2021 15:45:34 GMT +cache-control: public, max-age=7200 +accept-ranges: bytes +expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" +server: cloudflare +cf-ray: 695d69b549330686-LHR""".replace( + "\n", "\r\n" +) + class MyKissHeadersFromStringTest(unittest.TestCase): headers: Headers @@ -169,6 +202,12 @@ def test_fixed_type_output(self): self.assertEqual(str, type(headers.accept[-1].q)) + def test_parse_with_extra_connect(self): + headers: Headers = parse_it(RAW_HEADERS_WITH_CONNECT) + + self.assertTrue("Date" in headers) + self.assertTrue("Server" in headers) + if __name__ == "__main__": unittest.main()