diff --git a/README.md b/README.md index d4b318c..5643a67 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Modern python environment poetry, pyenv, pytest, pytest-cov, pytest-mock, nox, black, safety, pre-commit, mypy, pytype (or pyre, pyright), marshmallow, typeguard. -jsonargparse, httpx, cattrs & orjson +jsonargparse, httpx, cattrs[orjson] & orjson sphinx, sphinx-autodoc-typehints, git actions, codecov. And important, run in python v3.12 diff --git a/noxfile.py b/noxfile.py index 8ece396..0c0f798 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,7 +15,7 @@ def install_with_constraints( - session: nox.sessions.Session, *args: str, **kwargs: Any + session: nox.Session, *args: str, **kwargs: Any ) -> None: """Install packages constrained by Poetry's lock file.""" session.run( @@ -29,7 +29,7 @@ def install_with_constraints( session.install("--constraint=requirements.txt", *args, **kwargs) -def install(session, groups, root=True): +def install(session: nox.Session, groups: list[str], root: bool = True): if root: groups = ["main", *groups] session.run_install( @@ -87,7 +87,7 @@ def safety(session): """ -def constraints(session): +def constraints(session: nox.Session): filename = f"python{session.python}-{sys.platform}-{platform.machine()}.txt" return Path("constraints") / filename @@ -95,7 +95,7 @@ def constraints(session): @nox.session( python=["3.12", "3.11", "3.10", "3.9", "3.8", "3.7"], venv_backend="uv" ) -def lock(session): +def lock(session: nox.Session): """Lock the dependencies.""" filename = constraints(session) filename.parent.mkdir(exist_ok=True) @@ -112,7 +112,7 @@ def lock(session): @nox.session -def build(session): +def build(session: nox.Session): """Build the package.""" session.install("build", "twine") @@ -125,7 +125,7 @@ def build(session): @nox.session(python=["3.11"]) -def safety(session): +def safety(session: nox.Session): """Scan dependencies for insecure packages.""" session.run( "poetry", @@ -139,7 +139,7 @@ def safety(session): session.run("safety", "check", "--file=requirements.txt", "--full-report") -def install_coverage_pth(session): +def install_coverage_pth(session: nox.Session): output = session.run( "python", "-c", @@ -153,7 +153,7 @@ def install_coverage_pth(session): @nox.session(python=["3.11"]) -def tests(session): +def tests(session: nox.Session): """Run the test suite.""" args = session.posargs or ["--cov"] session.run("poetry", "install", external=True) @@ -161,7 +161,7 @@ def tests(session): @nox.session(python=["3.12", "3.11", "3.10", "3.9", "3.8", "3.7"]) -def tests_2(session): +def tests_2(session: nox.Session): """Run the test suite.""" session.install("-c", constraints(session), ".[tests]") install_coverage_pth(session) @@ -174,7 +174,7 @@ def tests_2(session): @nox.session(python="3.12") -def lint(session): +def lint(session: nox.Session): """Lint using pre-commit.""" options = ["--all-files", "--show-diff-on-fail"] session.install(f"--constraint={constraints(session)}", "pre-commit") @@ -199,7 +199,7 @@ def typeguard(session: nox.Session) -> None: @nox.session(python=["3.11"]) -def black(session): +def black(session: nox.Session): """Run black code formatter.""" args = session.posargs or locations session.install( @@ -209,7 +209,7 @@ def black(session): @nox.session(python=["3.11"]) -def pytype(session): +def pytype(session: nox.Session): """Type-check using pytype.""" args = session.posargs or ["--disable=import-error", *locations] session.install("pytype") @@ -217,7 +217,7 @@ def pytype(session): @nox.session(python=["3.11"]) -def xdoctest(session: nox.sessions.Session) -> None: +def xdoctest(session: nox.Session) -> None: """Run examples with xdoctest.""" args = session.posargs or ["all"] session.run("poetry", "install", "--no-dev", external=True) @@ -226,7 +226,7 @@ def xdoctest(session: nox.sessions.Session) -> None: @nox.session(python=["3.11"]) -def docs(session: nox.sessions.Session) -> None: +def docs(session: nox.Session) -> None: """Build the documentation.""" session.run("poetry", "install", "--no-dev", external=True) session.install("sphinx", "sphinx-autodoc-typehints") @@ -234,7 +234,7 @@ def docs(session: nox.sessions.Session) -> None: @nox.session(python=["3.11"]) -def coverage(session: nox.sessions.Session) -> None: +def coverage(session: nox.Session) -> None: """Generate the coverage report.""" session.install("-c", constraints(session), "coverage[toml]") if any(Path().glob(".coverage.*")): diff --git a/poetry.lock b/poetry.lock index c2e9e1b..5c9ed55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -74,6 +74,30 @@ files = [ [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +[[package]] +name = "cattrs" +version = "23.2.3" +description = "Composable complex class support for attrs and dataclasses." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, +] + +[package.dependencies] +attrs = ">=23.1.0" +orjson = {version = ">=3.9.2", optional = true, markers = "implementation_name == \"cpython\" and extra == \"orjson\""} + +[package.extras] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] + [[package]] name = "certifi" version = "2024.7.4" @@ -274,26 +298,6 @@ files = [ [package.extras] toml = ["tomli"] -[[package]] -name = "desert" -version = "2022.9.22" -description = "Deserialize to objects while staying DRY" -optional = false -python-versions = ">=3.6" -files = [ - {file = "desert-2022.9.22-py3-none-any.whl", hash = "sha256:cad7b6f1936448d26bde403882ec6855786b4d24d38d0ed4400e505ac8c5591f"}, - {file = "desert-2022.9.22.tar.gz", hash = "sha256:0f45e098915e16452a269a1da0bcbbf31df0af2173899f1c097fb2e3b0265d89"}, -] - -[package.dependencies] -attrs = "*" -marshmallow = ">=3.0" -typing-inspect = "*" - -[package.extras] -dev = ["black", "build", "bump2version", "check-manifest", "coverage", "cuvner", "docutils", "importlib-metadata", "isort", "marshmallow-enum", "marshmallow-union", "mypy", "pex", "pygments", "pylint", "pytest", "pytest-cov", "pytest-sphinx", "pytest-travis-fold", "readme-renderer", "towncrier", "tox", "twine", "versioneer", "wheel"] -test = ["coverage", "cuvner", "importlib-metadata", "marshmallow-enum", "marshmallow-union", "pytest", "pytest-cov", "pytest-sphinx", "pytest-travis-fold", "tox"] - [[package]] name = "distro" version = "1.9.0" @@ -763,25 +767,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "marshmallow" -version = "3.21.3" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.8" -files = [ - {file = "marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1"}, - {file = "marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662"}, -] - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] -docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] -tests = ["pytest", "pytz", "simplejson"] - [[package]] name = "mypy" version = "1.11.0" @@ -854,6 +839,66 @@ files = [ antlr4-python3-runtime = "==4.9.*" PyYAML = ">=5.1.0" +[[package]] +name = "orjson" +version = "3.10.6" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, + {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, + {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, + {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, + {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, + {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, + {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, + {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, + {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, + {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, + {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, + {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, + {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, + {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, + {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, +] + [[package]] name = "packaging" version = "24.1" @@ -1534,21 +1579,6 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -[[package]] -name = "typing-inspect" -version = "0.9.0" -description = "Runtime inspection utilities for typing module." -optional = false -python-versions = "*" -files = [ - {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, - {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, -] - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" - [[package]] name = "urllib3" version = "2.2.2" @@ -1623,4 +1653,4 @@ tests-strict = ["pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pyt [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5c618e217a9be576f6e29feb77ba2227e5f0eca80181f850d9cb3e4fd7dcc41f" +content-hash = "dded4974a56a911f59ba7da789a117b11176349363321d680795e7b8e5052ea2" diff --git a/pyproject.toml b/pyproject.toml index e7c3e51..91a12da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,12 +20,11 @@ include = ["tests"] [tool.poetry.dependencies] python = "^3.12" safety-db = "^2021.7.17" -desert = "^2022.9.22" -marshmallow = "^3.19.0" codecov = "^2.1.13" httpx = "^0.27.0" jsonargparse = {extras = ["all"], version = "^4.32.0"} loguru = "^0.7.2" +cattrs = {extras = ["orjson"], version = "^23.2.3"} [tool.poetry.group.dev.dependencies] diff --git a/src/hypermodern_python/__pycache__/prettier.cpython-312.pyc b/src/hypermodern_python/__pycache__/prettier.cpython-312.pyc new file mode 100644 index 0000000..63ffee6 Binary files /dev/null and b/src/hypermodern_python/__pycache__/prettier.cpython-312.pyc differ diff --git a/src/hypermodern_python/console.py b/src/hypermodern_python/console.py index a0565df..4569acc 100644 --- a/src/hypermodern_python/console.py +++ b/src/hypermodern_python/console.py @@ -1,9 +1,5 @@ """Command-line interface.""" -import textwrap - -from prettier import cprint - from . import wikipedia @@ -11,10 +7,3 @@ def cmd() -> None: from jsonargparse import CLI CLI(wikipedia.Fetcher) - - -def main(language: str) -> None: - """The hypermodern Python project.""" - page = wikipedia.random_page(language=language) - cprint(page.title, fg="g") - cprint(textwrap.fill(page.extract)) diff --git a/src/hypermodern_python/wikipedia.py b/src/hypermodern_python/wikipedia.py index ad13f7a..6a00713 100644 --- a/src/hypermodern_python/wikipedia.py +++ b/src/hypermodern_python/wikipedia.py @@ -1,30 +1,42 @@ """Client for the Wikipedia REST API, version 1.""" -import sys - -if sys.version_info >= (3, 8): - from importlib.metadata import metadata -else: - from importlib_metadata import metadata +from __future__ import annotations import asyncio +import sys +import textwrap +import time +from collections.abc import Awaitable from dataclasses import dataclass, field -from typing import Awaitable +from typing import TypeAlias -import desert +import cattrs.gen import httpx -import marshmallow from loguru import logger +from prettier import cprint + +if sys.version_info >= (3, 8): + pass # from importlib.metadata import metadata +else: + pass # from importlib_metadata import metadata + +if sys.version_info >= (3, 9): + from collections.abc import Iterable +else: + from typing import Iterable API_URL: str = ( "https://{language}.wikipedia.org/api/rest_v1/page/random/summary" ) -USER_AGENT: str = "{Name}/{Version} (Contact: {Author-email})" - +# API_URL: Final = "https://en.wikipedia.org/api/rest_v1/page/random/summary" +# USER_AGENT: str = "{Name}/{Version} (Contact: {Author-email})" +JSON: TypeAlias = ( + None | bool | int | float | str | list["JSON"] | dict[str, "JSON"] +) -def build_user_agent() -> str: - fields = metadata("hypermodern_python") - return USER_AGENT.format_map(fields) +# def build_user_agent() -> str: +# fields = metadata("hypermodern_python") +# return USER_AGENT.format_map(fields) @dataclass @@ -40,20 +52,17 @@ class Page: extract: str -schema = desert.schema(Page, meta={"unknown": marshmallow.EXCLUDE}) - - @dataclass(slots=True) class Fetcher: language: str = "en" url: str | None = None timeout: float | None = None - headers: dict | None + headers: dict | None = None # These two are marked 'init=False' so they do not show up in the constructor # noqa: E501 # logic because the user doesn't need the ability to initialize these values since # noqa: E501 # they a.) have defaults and b.) are internal implementation details. - client: httpx.Client = field( - default_factory=httpx.AsyncClient(), init=False + client: httpx.AsyncClient = field( + default_factory=httpx.AsyncClient, init=False ) results: list[str] = field(default_factory=list, init=False) @@ -65,12 +74,12 @@ def __post_init__(self) -> None: self.client.timeout = self.timeout if not self.url: self.url = API_URL.format(language=self.language) - if not self.client.headers and not self.headers: - self.client.headers = {"User-Agent": build_user_agent()} + if not self.client.headers and self.headers: + self.client.headers = self.headers self.client.http2 = True async def fetch( - self, func: Awaitable[httpx.AsyncClient(), str, str] + self, func: Awaitable[[httpx.AsyncClient, str, str], Page] ) -> None: async with self.client as client: tasks = [] @@ -80,19 +89,33 @@ async def fetch( func(client, self.url, language=self.language) ) ) - - self.results = await asyncio.gather(*tasks) + start_time = time.perf_counter() + self.results: Iterable[JSON] = await asyncio.gather(*tasks) + end_time = time.perf_counter() for page in self.results: - print(page) + cprint(page.title, fg="g") + cprint(textwrap.fill(page.extract), fg="r") + cprint("\n\n") logger.info( - "[{} :: {}] Mission complete", + "[{} :: {}] Mission complete in {} seconds", self.url, len(self.results), + end_time - start_time, ) +converter = cattrs.Converter() +converter.register_structure_hook( + Page, + cattrs.gen.make_dict_structure_fn( + Page, + converter, + ), +) + + async def random_page( - client: httpx.AsyncClient(), url: str, language: str = "en" + client: httpx.AsyncClient, url: str, language: str = "en" ) -> Page: """Return a random page. @@ -114,14 +137,16 @@ async def random_page( True """ try: - response = await client.get(url) + response = await client.get(url, follow_redirects=True) response.raise_for_status() - data = response.json() - return schema.load(data) - except marshmallow.ValidationError as error: + data: JSON = response.json() + return converter.structure(data, Page) + except httpx.HTTPStatusError as error: message = str(error) raise message +# headers = {"User-Agent": build_user_agent()} + fetch_wikipedia = Fetcher(timeout=100) asyncio.run(fetch_wikipedia.fetch(random_page))