From 5ec59bc607bd3392bb821ce2de729f0dc2b50f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Szafra=C5=84ski?= Date: Wed, 13 Dec 2023 13:43:15 +0100 Subject: [PATCH] Initialize authorization (#17) --- .pre-commit-config.yaml | 51 ++--- poetry.lock | 208 ++++++++++-------- pyproject.toml | 7 +- src/ksef/auth/init_session_token_request.py | 145 ++++++++++++ src/ksef/auth/token.py | 68 +++++- src/ksef/client.py | 43 ++++ src/ksef/constants.py | 5 +- src/ksef/models/invoice.py | 2 - .../models/responses/authorization_token.py | 81 +++++++ tests/auth/test_token.py | 12 +- 10 files changed, 496 insertions(+), 126 deletions(-) create mode 100644 src/ksef/auth/init_session_token_request.py create mode 100644 src/ksef/client.py create mode 100644 src/ksef/models/responses/authorization_token.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b2edf6..e2c7aae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,10 @@ repos: - repo: meta hooks: - id: check-useless-excludes + - repo: https://github.com/MarcoGorelli/absolufy-imports + rev: v0.3.1 + hooks: + - id: absolufy-imports - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 hooks: @@ -14,6 +18,17 @@ repos: - id: rst-backticks - id: rst-directive-colons - id: rst-inline-touching-normal + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.12.0 + hooks: + - id: black + language_version: python3.8 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.7 + hooks: + - id: ruff + args: [ "--fix", "--fixable=I001,ERA001,F401,F841,T201,T203" ] + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: @@ -42,34 +57,13 @@ repos: - id: no-commit-to-branch - id: trailing-whitespace types: [python] - - repo: local + - repo: https://github.com/commitizen-tools/commitizen + rev: v1.17.0 hooks: - id: commitizen - name: commitizen - entry: cz check - args: [--commit-msg-file] - require_serial: true - language: system - stages: [commit-msg] - - id: absolufy-imports - name: absolufy-imports - entry: absolufy-imports - require_serial: true - language: system - types: [python] - - id: ruff - name: ruff - entry: ruff - args: ["--fix", "--fixable=I001,ERA001,F401,F841,T201,T203"] - require_serial: true - language: system - types: [python] - - id: black - name: black - entry: black - require_serial: true - language: system - types: [python] + stages: [ commit-msg ] + - repo: local + hooks: - id: shellcheck name: shellcheck entry: shellcheck @@ -90,6 +84,7 @@ repos: pass_filenames: false - id: mypy name: mypy - entry: mypy + entry: poetry run mypy + require_serial: true language: system - types: [python] + types: [ python ] diff --git a/poetry.lock b/poetry.lock index 10509c2..bb40699 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "absolufy-imports" version = "0.3.1" description = "A tool to automatically replace relative imports with absolute ones." -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -16,7 +15,6 @@ files = [ name = "argcomplete" version = "2.0.6" description = "Bash tab completion for argparse" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -32,7 +30,6 @@ test = ["coverage", "flake8", "mypy", "pexpect", "wheel"] name = "arrow" version = "1.2.3" description = "Better dates & times for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -47,7 +44,6 @@ python-dateutil = ">=2.7.0" name = "astunparse" version = "1.6.3" description = "An AST unparser for Python" -category = "dev" optional = false python-versions = "*" files = [ @@ -63,7 +59,6 @@ wheel = ">=0.23.0,<1.0" name = "attrs" version = "22.2.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -82,7 +77,6 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy name = "binaryornot" version = "0.4.4" description = "Ultra-lightweight pure Python package to check if a file is binary or text." -category = "dev" optional = false python-versions = "*" files = [ @@ -97,7 +91,6 @@ chardet = ">=3.0.2" name = "black" version = "22.12.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -133,7 +126,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -141,11 +133,74 @@ files = [ {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -157,7 +212,6 @@ files = [ name = "chardet" version = "5.1.0" description = "Universal encoding detector for Python 3" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -169,7 +223,6 @@ files = [ name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -184,7 +237,6 @@ unicode-backport = ["unicodedata2"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -199,7 +251,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -211,7 +262,6 @@ files = [ name = "commitizen" version = "2.42.1" description = "Python commitizen client tool" -category = "dev" optional = false python-versions = ">=3.6.2,<4.0.0" files = [ @@ -236,7 +286,6 @@ typing-extensions = ">=4.0.1,<5.0.0" name = "cookiecutter" version = "2.1.1" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -257,7 +306,6 @@ requests = ">=2.23.0" name = "coverage" version = "7.2.2" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -324,7 +372,6 @@ toml = ["tomli"] name = "cruft" version = "2.12.0" description = "Allows you to maintain all the necessary cruft for packaging and building projects separate from the code you intentionally write. Built on-top of CookieCutter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -341,11 +388,55 @@ typer = ">=0.4.0" [package.extras] pyproject = ["toml (>=0.10)"] +[[package]] +name = "cryptography" +version = "41.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "decli" version = "0.5.2" description = "Minimal, easy-to-use, declarative cli tool" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -357,7 +448,6 @@ files = [ name = "distlib" version = "0.3.6" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" files = [ @@ -369,7 +459,6 @@ files = [ name = "dparse" version = "0.6.2" description = "A parser for Python dependency files" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -389,7 +478,6 @@ pipenv = ["pipenv"] name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -404,7 +492,6 @@ test = ["pytest (>=6)"] name = "execnet" version = "1.9.0" description = "execnet: rapid multi-Python deployment" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -419,7 +506,6 @@ testing = ["pre-commit"] name = "filelock" version = "3.10.0" description = "A platform independent file lock." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -435,7 +521,6 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.1)", "pytest (>=7.2.2)", "pyt name = "gitdb" version = "4.0.10" description = "Git Object Database" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -450,7 +535,6 @@ smmap = ">=3.0.1,<6" name = "gitpython" version = "3.1.31" description = "GitPython is a Python library used to interact with Git repositories" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -465,7 +549,6 @@ gitdb = ">=4.0.1,<5" name = "identify" version = "2.5.21" description = "File identification library for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -480,7 +563,6 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -492,7 +574,6 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -504,7 +585,6 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -522,7 +602,6 @@ i18n = ["Babel (>=2.7)"] name = "jinja2-time" version = "0.2.0" description = "Jinja2 Extension for Dates and Times" -category = "dev" optional = false python-versions = "*" files = [ @@ -538,7 +617,6 @@ jinja2 = "*" name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -563,7 +641,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -623,7 +700,6 @@ files = [ name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -635,7 +711,6 @@ files = [ name = "mypy" version = "1.1.1" description = "Optional static typing for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -682,7 +757,6 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -694,7 +768,6 @@ files = [ name = "nodeenv" version = "1.7.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -709,7 +782,6 @@ setuptools = "*" name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -724,7 +796,6 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" name = "pastel" version = "0.2.1" description = "Bring colors to your terminal." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -736,7 +807,6 @@ files = [ name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -748,7 +818,6 @@ files = [ name = "pdoc" version = "13.0.0" description = "API Documentation for Python Projects" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -769,7 +838,6 @@ dev = ["black", "hypothesis", "mypy", "pygments (>=2.14.0)", "pytest", "pytest-c name = "platformdirs" version = "3.1.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -785,7 +853,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytes name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -801,7 +868,6 @@ testing = ["pytest", "pytest-benchmark"] name = "poethepoet" version = "0.18.1" description = "A task runner that works well with poetry." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -820,7 +886,6 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] name = "pprintpp" version = "0.4.0" description = "A drop-in replacement for pprint that's actually pretty" -category = "dev" optional = false python-versions = "*" files = [ @@ -832,7 +897,6 @@ files = [ name = "pre-commit" version = "3.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -851,7 +915,6 @@ virtualenv = ">=20.10.0" name = "prompt-toolkit" version = "3.0.38" description = "Library for building powerful interactive command lines in Python" -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -862,11 +925,21 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" version = "1.10.6" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -919,7 +992,6 @@ email = ["email-validator (>=1.0.3)"] name = "pygments" version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -934,7 +1006,6 @@ plugins = ["importlib-metadata"] name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" optional = false python-versions = ">=3.6.8" files = [ @@ -949,7 +1020,6 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "7.2.2" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -973,7 +1043,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-clarity" version = "1.0.1" description = "A plugin providing an alternative, colourful diff output for failing assertions." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -989,7 +1058,6 @@ rich = ">=8.0.0" name = "pytest-mock" version = "3.10.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1007,7 +1075,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "pytest-responses" version = "0.5.1" description = "py.test integration for responses" -category = "dev" optional = false python-versions = "*" files = [ @@ -1025,7 +1092,6 @@ tests = ["flake8"] name = "pytest-xdist" version = "3.2.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1046,7 +1112,6 @@ testing = ["filelock"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1061,7 +1126,6 @@ six = ">=1.5" name = "python-slugify" version = "8.0.1" description = "A Python slugify application that also handles Unicode" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1079,7 +1143,6 @@ unidecode = ["Unidecode (>=1.1.1)"] name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1129,7 +1192,6 @@ files = [ name = "questionary" version = "1.10.0" description = "Python library to build pretty command line user prompts ⭐️" -category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -1147,7 +1209,6 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphin name = "requests" version = "2.28.2" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7, <4" files = [ @@ -1169,7 +1230,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "responses" version = "0.23.1" description = "A utility library for mocking out the `requests` Python library." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1190,7 +1250,6 @@ tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asy name = "rich" version = "13.3.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -1210,7 +1269,6 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "ruamel-yaml" version = "0.17.21" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -category = "dev" optional = false python-versions = ">=3" files = [ @@ -1229,7 +1287,6 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] name = "ruamel-yaml-clib" version = "0.2.7" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1275,7 +1332,6 @@ files = [ name = "ruff" version = "0.0.254" description = "An extremely fast Python linter, written in Rust." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1302,7 +1358,6 @@ files = [ name = "safety" version = "2.3.5" description = "Checks installed dependencies for known vulnerabilities and licenses." -category = "dev" optional = false python-versions = "*" files = [ @@ -1326,7 +1381,6 @@ gitlab = ["python-gitlab (>=1.3.0)"] name = "setuptools" version = "67.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1343,7 +1397,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "shellcheck-py" version = "0.9.0.2" description = "Python wrapper around invoking shellcheck (https://www.shellcheck.net/)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1357,7 +1410,6 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1369,7 +1421,6 @@ files = [ name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1381,7 +1432,6 @@ files = [ name = "termcolor" version = "2.2.0" description = "ANSI color formatting for output in terminal" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1396,7 +1446,6 @@ tests = ["pytest", "pytest-cov"] name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" -category = "dev" optional = false python-versions = "*" files = [ @@ -1408,7 +1457,6 @@ files = [ name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1420,7 +1468,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1432,7 +1479,6 @@ files = [ name = "tomlkit" version = "0.11.6" description = "Style preserving TOML library" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1444,7 +1490,6 @@ files = [ name = "typeguard" version = "2.13.3" description = "Run-time type checker for Python" -category = "dev" optional = false python-versions = ">=3.5.3" files = [ @@ -1460,7 +1505,6 @@ test = ["mypy", "pytest", "typing-extensions"] name = "typer" version = "0.7.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1481,7 +1525,6 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6. name = "types-pyyaml" version = "6.0.12.8" description = "Typing stubs for PyYAML" -category = "dev" optional = false python-versions = "*" files = [ @@ -1493,7 +1536,6 @@ files = [ name = "types-requests" version = "2.28.11.15" description = "Typing stubs for requests" -category = "dev" optional = false python-versions = "*" files = [ @@ -1508,7 +1550,6 @@ types-urllib3 = "<1.27" name = "types-urllib3" version = "1.26.25.8" description = "Typing stubs for urllib3" -category = "dev" optional = false python-versions = "*" files = [ @@ -1520,7 +1561,6 @@ files = [ name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1532,7 +1572,6 @@ files = [ name = "urllib3" version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1549,7 +1588,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "virtualenv" version = "20.21.0" description = "Virtual Python Environment builder" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1570,7 +1608,6 @@ test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess name = "wcwidth" version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1582,7 +1619,6 @@ files = [ name = "wheel" version = "0.40.0" description = "A built-package format for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1596,4 +1632,4 @@ test = ["pytest (>=6.0.0)"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "266d77bd75a233d914d828a97d205476f7e25c84a12e4ce8d434a9182c412a40" +content-hash = "a0782b4a8ca6eb10e263a09cc7fadc18c9f46304176fbfd013f5b936ae94934f" diff --git a/pyproject.toml b/pyproject.toml index 41c3600..7499645 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ version_files = ["pyproject.toml:version"] pydantic = "^1.10.5" python = "^3.8" requests = "^2.28.2" +cryptography = "^41.0.7" [tool.poetry.group.test.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/ absolufy-imports = "^0.3.1" @@ -71,12 +72,16 @@ plugins = "pydantic.mypy" strict = true disallow_subclassing_any = false disallow_untyped_decorators = false +disallow_untyped_calls = false ignore_missing_imports = true -pretty = true +pretty = false show_column_numbers = true show_error_codes = true show_error_context = true warn_unreachable = true +exclude = [ + "^tests/*" +] [tool.pydantic-mypy] # https://pydantic-docs.helpmanual.io/mypy_plugin/#configuring-the-plugin init_forbid_extra = true diff --git a/src/ksef/auth/init_session_token_request.py b/src/ksef/auth/init_session_token_request.py new file mode 100644 index 0000000..01a0739 --- /dev/null +++ b/src/ksef/auth/init_session_token_request.py @@ -0,0 +1,145 @@ +"""Builder for InitSessionTokenRequest xml document.""" +from typing import cast +from xml.dom import minidom + +from ksef.models.responses.authorization_challenge import AuthorizationChallenge + + +class InitSessionTokenRequestBuilder: + """Builder used to construct a XML request for initializing a session token.""" + + NS = "http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001" + NS2 = "http://ksef.mf.gov.pl/schema/gtw/svc/types/2021/10/01/0001" + NS3 = "http://ksef.mf.gov.pl/schema/gtw/svc/online/auth/request/2021/10/01/0001" + XSI = "http://www.w3.org/2001/XMLSchema-instance" + + def __init__( + self, authorization_challenge: AuthorizationChallenge, nip: str, encrypted_token: str + ): + self.authorization_challenge = authorization_challenge + self.nip = nip + self.encrypted_token = encrypted_token + + @staticmethod + def _build_document_type_element(root: minidom.Document) -> minidom.Element: + document_type = root.createElement("DocumentType") + service = root.createElement("ns2:Service") + service.appendChild(root.createTextNode("KSeF")) + document_type.appendChild(service) + + form_code = root.createElement("ns2:FormCode") + document_type.appendChild(form_code) + + system_code = root.createElement("ns2:SystemCode") + system_code.appendChild(root.createTextNode("FA (1)")) + form_code.appendChild(system_code) + schema_version = root.createElement("ns2:SchemaVersion") + schema_version.appendChild(root.createTextNode("1-0E")) + form_code.appendChild(schema_version) + target_namespace = root.createElement("ns2:TargetNamespace") + target_namespace.appendChild( + root.createTextNode("http://crd.gov.pl/wzor/2021/11/29/11089/") + ) + form_code.appendChild(target_namespace) + value = root.createElement("ns2:Value") + value.appendChild(root.createTextNode("FA")) + form_code.appendChild(value) + + return document_type + + def _build_token_element(self, root: minidom.Document) -> minidom.Element: + token = root.createElement("Token") + token.appendChild(root.createTextNode(self.encrypted_token)) + return token + + def _build_context_element(self, root: minidom.Document) -> minidom.Element: + context = root.createElement("ns3:Context") + + challenge = root.createElement("Challenge") + challenge.appendChild(root.createTextNode(self.authorization_challenge.challenge)) + context.appendChild(challenge) + + identifier = root.createElement("Identifier") + identifier.setAttribute("xmlns:xsi", self.XSI) + identifier.setAttribute("xsi:type", "ns2:SubjectIdentifierByCompanyType") + + identifier_inner = root.createElement("ns2:Identifier") + identifier_inner.appendChild(root.createTextNode(self.nip)) + identifier.appendChild(identifier_inner) + context.appendChild(identifier) + + context.appendChild(self._build_document_type_element(root=root)) + context.appendChild(self._build_token_element(root=root)) + + return context + + def _build_signature_element(self, root: minidom.Document) -> minidom.Element: + signature = root.createElement("Signature") + signature.setAttribute("xmlns", "http://www.w3.org/2000/09/xmldsig#") + + signed_info = root.createElement("SignedInfo") + + canonicalization_method = root.createElement("CanonicalizationMethod") + canonicalization_method.setAttribute( + "Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" + ) + signed_info.appendChild(canonicalization_method) + + signature_method = root.createElement("SignatureMethod") + signature_method.setAttribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1") + signed_info.appendChild(signature_method) + + reference = root.createElement("Reference") + reference.setAttribute("URI", "") + signed_info.appendChild(reference) + + transforms = root.createElement("Transforms") + reference.appendChild(transforms) + + transform = root.createElement("Transform") + transform.setAttribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature") + transforms.appendChild(transform) + + digest_method = root.createElement("DigestMethod") + digest_method.setAttribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1") + reference.appendChild(digest_method) + + digest_value = root.createElement("DigestValue") + calculated_digest_value = "XXX" # TODO + digest_value.appendChild(root.createTextNode(calculated_digest_value)) + reference.appendChild(digest_value) + + signature.appendChild(signed_info) + + calculated_signature_value = "XXX" # TODO + signature_value = root.createElement("SignatureValue") + signature_value.appendChild(root.createTextNode(calculated_signature_value)) + + signature.appendChild(signature_value) + + key_info = root.createElement("KeyInfo") + signature.appendChild(key_info) + + x509_data = root.createElement("X509Data") + key_info.appendChild(x509_data) + + calculated_x509_certificate = "XXX" # TODO + x509_certificate = root.createElement("X509Certificate") + x509_certificate.appendChild(root.createTextNode(calculated_x509_certificate)) + x509_data.appendChild(x509_certificate) + return signature + + def build_xml(self) -> str: + """Build and return an XML string representing a request to initialize a session token.""" + root = minidom.Document() + + document = root.createElement("ns3:InitSessionTokenRequest") + document.setAttribute("xmlns", self.NS) + document.setAttribute("xmlns:ns2", self.NS2) + document.setAttribute("xmlns:ns3", self.NS3) + root.appendChild(document) + + context = self._build_context_element(root=root) + document.appendChild(context) + + return cast(str, root.toprettyxml(indent=" ", encoding="UTF-8").decode("utf-8")) diff --git a/src/ksef/auth/token.py b/src/ksef/auth/token.py index 7be93ae..0d26c93 100644 --- a/src/ksef/auth/token.py +++ b/src/ksef/auth/token.py @@ -1,22 +1,38 @@ """Simple token-based authorization implementation.""" +import base64 import copy -from typing import Mapping +from datetime import datetime, timezone +from typing import Mapping, cast from urllib.parse import urljoin import requests +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa from requests import Request from ksef.auth.base import Authorization -from ksef.constants import BASE_URL, DEFAULT_HEADERS, TIMEOUT, URL_SESSION_CHALLENGE +from ksef.auth.init_session_token_request import InitSessionTokenRequestBuilder +from ksef.constants import ( + BASE_URL, + DEFAULT_HEADERS, + TIMEOUT, + URL_AUTH_CHALLENGE, + URL_AUTH_INIT_TOKEN, +) from ksef.models.responses.authorization_challenge import AuthorizationChallenge +from ksef.models.responses.authorization_token import AuthorizationToken class TokenAuthorization(Authorization): """Simple token-based authorization.""" - def __init__(self, token: str, base_url: str = BASE_URL): + def __init__( + self, token: str, public_key: str, base_url: str = BASE_URL, timeout: int = TIMEOUT + ): self.base_url = base_url + self.public_key = public_key self.token = token + self.timeout = timeout def modify_request(self, request: Request) -> Request: """Enrich requests with authorization headers. @@ -25,6 +41,8 @@ def modify_request(self, request: Request) -> Request: """ request.prepare() request.headers["Authorization"] = self.token # TODO: This is just a stub implementaion + headers = self.build_headers() + request.headers.update(headers) return request def build_url(self, url: str) -> str: @@ -45,7 +63,7 @@ def build_headers(**optional: str) -> Mapping[str, str]: def get_authorization_challenge(self, nip: str) -> AuthorizationChallenge: """Get the token flow authorization challenge.""" response = requests.post( - url=self.build_url(URL_SESSION_CHALLENGE), + url=self.build_url(URL_AUTH_CHALLENGE), headers=self.build_headers(), json={ "contextIdentifier": { @@ -59,3 +77,45 @@ def get_authorization_challenge(self, nip: str) -> AuthorizationChallenge: return AuthorizationChallenge( timestamp=challenge["timestamp"], challenge=challenge["challenge"] ) + + def _encrypt_token(self, authorization_challenge: AuthorizationChallenge) -> str: + public_key = serialization.load_pem_public_key(self.public_key.encode()) + public_key = cast(rsa.RSAPublicKey, public_key) + + timestamp = ( + int( + datetime.strptime(authorization_challenge.timestamp, "%Y-%m-%dT%H:%M:%S.%fZ") + .replace(tzinfo=timezone.utc) + .timestamp() + ) + * 1000 + ) + message = self.token.encode() + b"|" + str(timestamp).encode() + encrypted_message = public_key.encrypt(plaintext=message, padding=padding.PKCS1v15()) + return base64.b64encode(encrypted_message).decode("utf-8") + + def _build_init_token_xml( + self, nip: str, authorization_challenge: AuthorizationChallenge + ) -> str: + encrypted_token = self._encrypt_token(authorization_challenge=authorization_challenge) + + request_builder = InitSessionTokenRequestBuilder( + authorization_challenge=authorization_challenge, + nip=nip, + encrypted_token=encrypted_token, + ) + return request_builder.build_xml() + + def init_token( + self, authorization_challenge: AuthorizationChallenge, nip: str + ) -> AuthorizationToken: + """Initialize the session.""" + document_xml = self._build_init_token_xml( + nip=nip, authorization_challenge=authorization_challenge + ) + response = requests.post( + url=self.build_url(URL_AUTH_INIT_TOKEN), + data=document_xml, + timeout=self.timeout, + ) + return AuthorizationToken.from_dict(response.json()) diff --git a/src/ksef/client.py b/src/ksef/client.py new file mode 100644 index 0000000..4742c67 --- /dev/null +++ b/src/ksef/client.py @@ -0,0 +1,43 @@ +"""Base client for interacting with the KSEF API.""" +from typing import Dict, Mapping, Optional, Union, cast +from urllib.parse import urlencode, urljoin + +import requests +from requests import Request + +from ksef.auth.base import Authorization +from ksef.constants import BASE_URL, URL_QUERY_INVOICES + + +class Client: + """Base client for interacting with the KSEF API.""" + + def __init__(self, authorization: Authorization, base_url: str = BASE_URL): + self.authorization = authorization + self.base_url = base_url + self.session = requests.Session() + + def build_url(self, url: str, params: Optional[Mapping[str, Union[str, int]]] = None) -> str: + """Construct a full URL.""" + url = urljoin(base=self.base_url, url=url) + if params is not None: + param_str = urlencode(params) + return f"{url}?{param_str}" + + return url + + def search_invoices(self, page_size: int = 100, page_offset: int = 0) -> Dict[str, str]: + """Search for invoices with the specified page size and offset.""" + params = { + "PageSize": page_size, + "PageOffset": page_offset, + } + request = Request( + method="POST", + url=self.build_url(url=URL_QUERY_INVOICES, params=params), + ) + request = self.authorization.modify_request(request) + prepared_request = request.prepare() + response = self.session.send(prepared_request) + data = cast(Dict[str, str], response.json()) + return data diff --git a/src/ksef/constants.py b/src/ksef/constants.py index b82609d..ef4a281 100644 --- a/src/ksef/constants.py +++ b/src/ksef/constants.py @@ -5,4 +5,7 @@ BASE_URL = "https://ksef-demo.mf.gov.pl/api/" # TODO: Change to prod TIMEOUT = 30 -URL_SESSION_CHALLENGE = "online/Session/AuthorisationChallenge" +URL_AUTH_CHALLENGE = "online/Session/AuthorisationChallenge" +URL_AUTH_INIT_TOKEN = "online/Session/InitToken" # noqa: S105 + +URL_QUERY_INVOICES = "online/Query/Invoice/Sync" diff --git a/src/ksef/models/invoice.py b/src/ksef/models/invoice.py index c748ade..fe6d15b 100644 --- a/src/ksef/models/invoice.py +++ b/src/ksef/models/invoice.py @@ -4,5 +4,3 @@ class Invoice(BaseModel): """Single invoice model.""" - - ... diff --git a/src/ksef/models/responses/authorization_token.py b/src/ksef/models/responses/authorization_token.py new file mode 100644 index 0000000..f91fc0f --- /dev/null +++ b/src/ksef/models/responses/authorization_token.py @@ -0,0 +1,81 @@ +"""Models for authorization token response.""" +from dataclasses import dataclass +from typing import Any, Dict, List + + +@dataclass +class ContextIdentifier: # noqa: D101 + type: str # noqa: A003 + identifier: str + + @classmethod + def from_dict(cls, data: Dict[str, str]) -> "ContextIdentifier": # noqa: D102 + return ContextIdentifier(type=data["type"], identifier=data["identifier"]) + + +@dataclass +class ContextName: # noqa: D101 + full_name: str + + +@dataclass +class CredentialRole: # noqa: D101 + type: str # noqa: A003 + role_type: str + role_description: str + start_timestamp: str + + @classmethod + def from_list(cls, data: List[Dict[str, str]]) -> List["CredentialRole"]: # noqa: D102 + return [cls.from_dict(cr) for cr in data] + + @classmethod + def from_dict(cls, data: Dict[str, str]) -> "CredentialRole": # noqa: D102 + return cls( + type=data["type"], + role_type=data["roleType"], + role_description=data["roleDescription"], + start_timestamp=data["startTimestamp"], + ) + + +@dataclass +class Context: # noqa: D101 + context_identifier: ContextIdentifier + context_name: ContextName + credentials_role_list: List[CredentialRole] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Context": # noqa: D102 + return cls( + context_identifier=ContextIdentifier.from_dict(data["contextIdentifier"]), + context_name=ContextName(full_name=data["contextName"]["fullName"]), + credentials_role_list=CredentialRole.from_list(data["credentialsRoleList"]), + ) + + +@dataclass +class SessionToken: # noqa: D101 + token: str + context: Context + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SessionToken": # noqa: D102 + return cls(token=data["token"], context=Context.from_dict(data["context"])) + + +@dataclass +class AuthorizationToken: + """Session token for authorization.""" + + timestamp: str + reference_number: str + session_token: SessionToken + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthorizationToken": # noqa: D102 + return cls( + timestamp=data["timestamp"], + reference_number=data["referenceNumber"], + session_token=SessionToken.from_dict(data["sessionToken"]), + ) diff --git a/tests/auth/test_token.py b/tests/auth/test_token.py index 399bd86..c580d18 100644 --- a/tests/auth/test_token.py +++ b/tests/auth/test_token.py @@ -5,14 +5,14 @@ from responses import RequestsMock from ksef.auth.token import TokenAuthorization -from ksef.constants import BASE_URL, URL_SESSION_CHALLENGE +from ksef.constants import BASE_URL, URL_AUTH_CHALLENGE from ksef.models.responses.authorization_challenge import AuthorizationChallenge def test_enrich() -> None: """Test if simple enrichment works.""" request = Request("POST", url="https://example.com") - auth = TokenAuthorization(token="abc123") # noqa: S106 + auth = TokenAuthorization(token="abc123", public_key="irrelevant") # noqa: S106 request = auth.modify_request(request) @@ -24,7 +24,7 @@ def test_get_authorization_challenge(mocked_responses: RequestsMock) -> None: timestamp = "2023-03-20T10:02:54.960Z" challenge_digest = "20230320-CR-3B5DCC20B3-C026645D90-3C" mocked_responses.add( - url=urljoin(BASE_URL, URL_SESSION_CHALLENGE), + url=urljoin(BASE_URL, URL_AUTH_CHALLENGE), method="POST", content_type="application/json", json={ @@ -32,7 +32,11 @@ def test_get_authorization_challenge(mocked_responses: RequestsMock) -> None: "challenge": challenge_digest, }, ) - auth = TokenAuthorization(token="abc123", base_url=BASE_URL) # noqa: S106 + auth = TokenAuthorization( + token="abc123", # noqa: S106 + base_url=BASE_URL, + public_key="irrelevant", + ) challenge = auth.get_authorization_challenge(nip="1234567890") assert isinstance(challenge, AuthorizationChallenge)