feat(services): integrate pre-commit hooks and enhance Medium API configuration

This commit is contained in:
ZhymabekRoman 2024-11-08 15:04:32 +05:00
parent 411095ceb0
commit 76bb8b3c1d
20 changed files with 757 additions and 19 deletions

View file

@ -0,0 +1,15 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: debug-statements
- id: check-toml
- id: check-json
- id: check-xml
- id: check-added-large-files
- id: check-ast
- id: check-merge-conflict

View file

@ -5,7 +5,7 @@
groups = ["default", "dev"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:cbe7c4d9cd8848458b3af09be1a95005fbc4adf178dd7a96f83810a4b45bff66"
content_hash = "sha256:f33cda53180165f833ddded2b624db8f9a24af2e825c9e137db33c5813de495d"
[[metadata.targets]]
requires_python = "==3.12.*"
@ -38,6 +38,17 @@ files = [
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
[[package]]
name = "cfgv"
version = "3.4.0"
requires_python = ">=3.8"
summary = "Validate configuration and produce human readable error messages."
groups = ["dev"]
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "click"
version = "8.1.7"
@ -65,6 +76,29 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "deprecation"
version = "2.1.0"
summary = "A library to handle automated deprecations"
groups = ["default"]
dependencies = [
"packaging",
]
files = [
{file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"},
{file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"},
]
[[package]]
name = "distlib"
version = "0.3.9"
summary = "Distribution utilities"
groups = ["dev"]
files = [
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
]
[[package]]
name = "execnet"
version = "2.1.1"
@ -91,6 +125,17 @@ files = [
{file = "faker-30.8.1.tar.gz", hash = "sha256:93e8b70813f76d05d98951154681180cb795cfbcff3eced7680d963bcc0da2a9"},
]
[[package]]
name = "filelock"
version = "3.16.1"
requires_python = ">=3.8"
summary = "A platform independent file lock."
groups = ["dev"]
files = [
{file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
{file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
]
[[package]]
name = "h11"
version = "0.14.0"
@ -138,6 +183,17 @@ files = [
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
]
[[package]]
name = "identify"
version = "2.6.1"
requires_python = ">=3.8"
summary = "File identification library for Python"
groups = ["dev"]
files = [
{file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
{file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
]
[[package]]
name = "idna"
version = "3.10"
@ -257,17 +313,86 @@ files = [
{file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Node.js virtual environment builder"
groups = ["dev"]
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "orjson"
version = "3.10.11"
requires_python = ">=3.8"
summary = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
groups = ["default", "dev"]
files = [
{file = "orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433"},
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5"},
{file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd"},
{file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b"},
{file = "orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d"},
{file = "orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284"},
{file = "orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef"},
]
[[package]]
name = "packaging"
version = "24.1"
requires_python = ">=3.8"
summary = "Core utilities for Python packages"
groups = ["dev"]
groups = ["default", "dev"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pendulum"
version = "3.0.0"
requires_python = ">=3.8"
summary = "Python datetimes made easy"
groups = ["default"]
dependencies = [
"backports-zoneinfo>=0.2.1; python_version < \"3.9\"",
"importlib-resources>=5.9.0; python_version < \"3.9\"",
"python-dateutil>=2.6",
"time-machine>=2.6.0; implementation_name != \"pypy\"",
"tzdata>=2020.1",
]
files = [
{file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"},
{file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"},
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"},
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"},
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"},
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"},
{file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"},
{file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"},
{file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"},
{file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"},
{file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"},
]
[[package]]
name = "platformdirs"
version = "4.3.6"
requires_python = ">=3.8"
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
groups = ["dev"]
files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[[package]]
name = "pluggy"
version = "1.5.0"
@ -294,6 +419,24 @@ files = [
{file = "polyfactory-2.17.0.tar.gz", hash = "sha256:099d86f7c79c51a2caaf7c8598cc56e7b0a57c11b5918ddf699e82380735b6b7"},
]
[[package]]
name = "pre-commit"
version = "4.0.1"
requires_python = ">=3.9"
summary = "A framework for managing and maintaining multi-language pre-commit hooks."
groups = ["dev"]
dependencies = [
"cfgv>=2.0.0",
"identify>=1.0.0",
"nodeenv>=0.11.1",
"pyyaml>=5.1",
"virtualenv>=20.10.0",
]
files = [
{file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"},
{file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"},
]
[[package]]
name = "psutil"
version = "6.1.0"
@ -370,6 +513,17 @@ files = [
{file = "pytest_httpx-0.33.0.tar.gz", hash = "sha256:4af9ab0dae5e9c14cb1e27d18af3db1f627b2cf3b11c02b34ddf26aff6b0a24c"},
]
[[package]]
name = "pytest-integration"
version = "0.2.3"
requires_python = ">=3.6"
summary = "Organizing pytests by integration or not"
groups = ["dev"]
files = [
{file = "pytest_integration-0.2.3-py3-none-any.whl", hash = "sha256:7f59ed1fa1cc8cb240f9495b68bc02c0421cce48589f78e49b7b842231604b12"},
{file = "pytest_integration-0.2.3.tar.gz", hash = "sha256:b00988a5de8a6826af82d4c7a3485b43fbf32c11235e9f4a8b7225eef5fbcf65"},
]
[[package]]
name = "pytest-xdist"
version = "3.6.1"
@ -420,7 +574,7 @@ name = "pyyaml"
version = "6.0.2"
requires_python = ">=3.8"
summary = "YAML parser and emitter for Python"
groups = ["default"]
groups = ["default", "dev"]
files = [
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
@ -489,6 +643,31 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "time-machine"
version = "2.16.0"
requires_python = ">=3.9"
summary = "Travel through time in your tests."
groups = ["default"]
marker = "implementation_name != \"pypy\""
dependencies = [
"python-dateutil",
]
files = [
{file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:84788f4d62a8b1bf5e499bb9b0e23ceceea21c415ad6030be6267ce3d639842f"},
{file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:15ec236b6571730236a193d9d6c11d472432fc6ab54e85eac1c16d98ddcd71bf"},
{file = "time_machine-2.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cedc989717c8b44a3881ac3d68ab5a95820448796c550de6a2149ed1525157f0"},
{file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d26d79de1c63a8c6586c75967e09b0ff306aa7e944a1eaddb74595c9b1839ca"},
{file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317b68b56a9c3731e0cf8886e0f94230727159e375988b36c60edce0ddbcb44a"},
{file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e1e18279759897be3293a255d53e6b1cb0364b69d9591d0b80c51e461c94b0"},
{file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e43adb22def972a29d2b147999b56897116085777a0fea182fd93ee45730611e"},
{file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0c766bea27a0600e36806d628ebc4b47178b12fcdfb6c24dc0a566a9c06bfe7f"},
{file = "time_machine-2.16.0-cp312-cp312-win32.whl", hash = "sha256:6dae82ab647d107817e013db82223e20a9853fa88543fec853ae326382d03c2e"},
{file = "time_machine-2.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:265462c77dc9576267c3c7f20707780a171a9fdbac93ac22e608c309efd68c33"},
{file = "time_machine-2.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:ef768e14768eebe3bb1196c0dece8e14c1c6991605721214a0c3c68cf77eb216"},
{file = "time_machine-2.16.0.tar.gz", hash = "sha256:4a99acc273d2f98add23a89b94d4dd9e14969c01214c8514bfa78e4e9364c7e2"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
@ -499,3 +678,51 @@ files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "tzdata"
version = "2024.2"
requires_python = ">=2"
summary = "Provider of IANA time zone data"
groups = ["default"]
files = [
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
]
[[package]]
name = "ujson"
version = "5.10.0"
requires_python = ">=3.8"
summary = "Ultra fast JSON encoder and decoder for Python"
groups = ["default"]
files = [
{file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"},
{file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"},
{file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"},
{file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"},
{file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"},
{file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"},
{file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"},
{file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"},
{file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"},
{file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"},
{file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"},
]
[[package]]
name = "virtualenv"
version = "20.27.1"
requires_python = ">=3.8"
summary = "Virtual Python Environment builder"
groups = ["dev"]
dependencies = [
"distlib<1,>=0.3.7",
"filelock<4,>=3.12.2",
"importlib-metadata>=6.6; python_version < \"3.8\"",
"platformdirs<5,>=3.9.1",
]
files = [
{file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"},
{file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"},
]

View file

@ -7,6 +7,10 @@ authors = [
]
dependencies = [
"litestar>=2.12.1",
"deprecation>=2.1.0",
"orjson>=3.10.11",
"pendulum>=3.0.0",
"ujson>=5.10.0",
]
requires-python = "==3.12.*"
readme = "README.md"
@ -21,4 +25,7 @@ dev = [
"pytest-asyncio>=0.24.0",
"pytest-httpx>=0.33.0",
"pytest-xdist[psutil]>=3.6.1",
"orjson>=3.10.11",
"pytest-integration>=0.2.3",
"pre-commit>=4.0.1",
]

View file

@ -1,10 +1,15 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from dependency_injector.wiring import Provide
from loguru import logger
from freedium_library.container import Container
from freedium_library.utils.http import Request
if TYPE_CHECKING:
from freedium_library.utils.http import Request
class BaseService(ABC):

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,10 @@
from __future__ import annotations
from typing import Optional
from pydantic import Field
from pydantic_settings import BaseSettings
class MediumConfig(BaseSettings):
cookies: Optional[str] = Field(default=None)

View file

@ -1,9 +1,18 @@
from dependency_injector import containers, providers
from freedium_library.utils.http import Request
from .api import MediumApiService
from .config import MediumConfig
from .validators import MediumServicePathValidator
class MediumContainer(containers.DeclarativeContainer):
medium_api_service = providers.Singleton(MediumApiService)
medium_path_validator = providers.Singleton(MediumServicePathValidator)
config = providers.Singleton(MediumConfig)
request = providers.Singleton(Request)
api_service = providers.Singleton(
MediumApiService,
request=request,
config=config,
)
validator = providers.Singleton(MediumServicePathValidator)

View file

@ -21,10 +21,8 @@ class MediumService(BaseService):
def __init__(
self,
request: Request = Provide[Container.request],
api_service: MediumApiService = Provide[MediumContainer.medium_api_service],
path_validator: MediumServicePathValidator = Provide[
MediumContainer.medium_path_validator
],
api_service: MediumApiService = Provide[MediumContainer.api_service],
path_validator: MediumServicePathValidator = Provide[MediumContainer.validator],
):
self.request = request
self.api_service = api_service

View file

@ -0,0 +1,34 @@
medium.com
uxplanet.org
osintteam.blog
ahmedelfakharany.com
drlee.io
generativeai.pub
productcoalition.com
towardsdev.com
infosecwriteups.com
towardsdatascience.com
thetaoist.online
devopsquare.com
bettermarketing.pub
itnext.io
betterprogramming.pub
curiouse.co
betterhumans.pub
uxdesign.cc
thebolditalic.com
codeburst.io
writingcooperative.com
entrepreneurshandbook.co
storiusmag.com
javascript.plainenglish.io
code.likeagirl.io
medium.datadriveninvestor.com
blog.det.life
python.plainenglish.io
blog.stackademic.com
ai.gopubby.com
blog.devops.dev
levelup.gitconnected.com
betterhumans.coach.me
ai.plainenglish.io

View file

@ -0,0 +1,58 @@
import os
from typing import Optional
import httpx
import pytest
from bs4 import BeautifulSoup
from loguru import logger
def check_medium_meta_tag(domain: str) -> Optional[bool]:
url = f"https://{domain}"
logger.info(f"Checking Medium meta tag for domain: {domain}")
try:
with httpx.Client(timeout=10.0) as client:
logger.debug(f"Sending GET request to {url}")
response = client.get(url, follow_redirects=True)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
meta_tag = soup.find(
"meta",
{"property": "al:android:package", "content": "com.medium.reader"},
)
if meta_tag is not None:
logger.success(f"Medium meta tag found for domain: {domain}")
return True
else:
logger.warning(f"No Medium meta tag found for domain: {domain}")
return False
except (httpx.RequestError, httpx.HTTPStatusError) as e:
logger.error(f"Error checking {domain}: {str(e)}")
return None
def get_domains_from_file(filepath: str) -> list[str]:
logger.info(f"Reading domains from file: {filepath}")
with open(filepath, "r") as file:
domains = [line.strip() for line in file if line.strip()]
logger.debug(f"Loaded {len(domains)} domains from file")
return domains
@pytest.mark.integration
@pytest.mark.parametrize(
"domain",
get_domains_from_file(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "medium_domains.txt")
),
)
def test_medium_domain_meta_tag(domain: str):
logger.info(f"Starting test for domain: {domain}")
result = check_medium_meta_tag(domain)
assert result is not None, f"Failed to check domain {domain}"
assert result is True, f"Domain {domain} does not have Medium meta tag"
logger.success(f"Test passed for domain: {domain}")

View file

@ -1,10 +1,12 @@
from __future__ import annotations
import re
import string
from typing import TYPE_CHECKING, Any, Optional
from urllib.parse import parse_qs, urlparse
from dependency_injector.wiring import Provide
from deprecation import deprecated
from loguru import logger
from freedium_library.container import Container
@ -65,7 +67,7 @@ class _MediumServiceURLValidator:
parsed_url = urlparse(url)
parsed_netloc = URLProcessor.un_wwwify(parsed_url.netloc)
if parsed_url.path.startswith("/p/"):
if parsed_url.path.startswith("/p/"): # TODO: add more information
logger.debug("URL is Medium 'mobile' link")
post_id = parsed_url.path.rsplit("/p/")[1]
@ -83,7 +85,7 @@ class _MediumServiceURLValidator:
elif (
parsed_netloc == "webcache.googleusercontent.com"
and parsed_url.path.startswith("/search")
):
): # TODO: is'n it deprecated? https://www.seozoom.com/google-cache/
logger.debug("URL seems like is Google Web Archive page link")
parsed_query = parse_qs(parsed_url.query)
@ -174,8 +176,31 @@ class _MediumServiceURLValidator:
class _MediumServiceHashesValidator:
def is_valid(self, hash: str) -> bool:
return bool(self.extract_hashes(hash))
VALID_ID_CHARS = set(string.ascii_letters + string.digits)
def is_valid(self, path: str) -> bool:
return bool(self.extract_hashes(path))
@deprecated(details="Use is_valid instead")
def is_valid_old(self, hex_string: str) -> bool:
# Check if the string is a valid hexadecimal string
for char in hex_string:
if char not in self.VALID_ID_CHARS:
return False
# Unfortunately, this logic doesn't works correctly sometimes, because
# there is some unique URLs that are has only digits, like this:
# https://valeman.medium.com/python-vs-r-for-time-series-forecasting-395390432598
# Check if the string contains only lowercase hexadecimal characters
# if not hex_string.islower():
# return False
# Check if the length of the string is correct for a hexadecimal string (e.g., 10, 11 or 12 characters)
if len(hex_string) not in range(8, 12 + 1):
return False
return True
def extract_hashes(self, path: str) -> list[str]:
logger.debug(f"Extracting hashes from path: {path}")

View file

@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, Mock
import pytest
from freedium_library.services.medium.api import MediumApiService
from freedium_library.services.medium.config import MediumConfig
from freedium_library.services.medium.validators import (
_MediumServiceHashesValidator, # type: ignore
_MediumServiceURLValidator, # type: ignore
@ -117,7 +118,8 @@ async def test_resolve_medium_url_with_real_short_link(
@pytest.mark.asyncio
async def test_resolve_medium_url_with_real_short_link_integration() -> None:
request = Request()
api_service = MediumApiService(request=request)
config = MediumConfig()
api_service = MediumApiService(request=request, config=config)
hash_validator = _MediumServiceHashesValidator()
url_validator = _MediumServiceURLValidator(api_service, hash_validator, request)

View file

@ -0,0 +1,4 @@
from .hash import HashLib
from .json import JSON, json_instance
__all__ = ["json_instance", "JSON", "HashLib"]

View file

@ -0,0 +1,14 @@
import hashlib
import secrets
class HashLib:
@staticmethod
def sha256(data: bytes) -> str:
sha256_hash = hashlib.sha256()
sha256_hash.update(data)
return sha256_hash.hexdigest()
@staticmethod
def random_sha256() -> str:
return HashLib.sha256(secrets.token_bytes())

View file

@ -1,4 +1,3 @@
import asyncio
import warnings
from dataclasses import dataclass
from typing import Any, Dict, Optional
@ -68,7 +67,7 @@ class Request:
def __del__(self):
self._client.close()
asyncio.run(self._async_client.aclose())
# asyncio.run(self._async_client.aclose()) # TODO: doesnt works
def _check_context_manager(self):
if not self._in_context_manager:

View file

@ -0,0 +1,130 @@
from abc import ABC, abstractmethod
from typing import Any, Optional, Type
from loguru import logger
class JSONBackend(ABC):
@abstractmethod
def dumps(self, obj: Any, pretty: bool = False) -> str:
pass
@abstractmethod
def loads(self, string: str) -> Any:
pass
@property
@abstractmethod
def name(self) -> str:
pass
def __repr__(self) -> str:
return f"{self.__class__.__name__}(backend='{self.name}')"
def __str__(self) -> str:
return f"JSON Backend: {self.name}"
class OrjsonBackend(JSONBackend):
def __init__(self):
import orjson
self._orjson = orjson
def dumps(self, obj: Any, pretty: bool = False) -> str:
opts = self._orjson.OPT_INDENT_2 if pretty else None
return self._orjson.dumps(obj, option=opts).decode("utf-8")
def loads(self, string: str) -> Any:
return self._orjson.loads(string)
@property
def name(self) -> str:
return "orjson"
class UjsonBackend(JSONBackend):
def __init__(self):
import ujson
self._ujson = ujson
def dumps(self, obj: Any, pretty: bool = False) -> str:
return self._ujson.dumps(obj, indent=2 if pretty else 0)
def loads(self, string: str) -> Any:
return self._ujson.loads(string)
@property
def name(self) -> str:
return "ujson"
class StandardJSONBackend(JSONBackend):
def __init__(self):
import json
self._json = json
def dumps(self, obj: Any, pretty: bool = False) -> str:
return self._json.dumps(obj, indent=2 if pretty else None)
def loads(self, string: str) -> Any:
return self._json.loads(string)
@property
def name(self) -> str:
return "json"
class JSON:
_instance: Optional[Type["JSON"]] = None
_backend: Optional[JSONBackend] = None
def __init__(self, backend: Optional[JSONBackend] = None) -> None:
if backend:
JSON._backend = backend
elif not JSON._backend:
JSON._backend = JSON._get_best_backend()
@classmethod
def _ensure_backend(cls) -> None:
if cls._backend is None:
cls._backend = cls._get_best_backend()
@classmethod
def dumps(cls, obj: Any, pretty: bool = False) -> str:
cls._ensure_backend()
return cls._backend.dumps(obj, pretty) # type: ignore
@classmethod
def loads(cls, string: str) -> Any:
cls._ensure_backend()
return cls._backend.loads(string) # type: ignore
@classmethod
def backend(cls) -> str:
cls._ensure_backend()
return cls._backend.name # type: ignore
@classmethod
def _get_best_backend(cls) -> JSONBackend:
backends = [
(OrjsonBackend, "orjson"),
(UjsonBackend, "ujson"),
(StandardJSONBackend, "json"),
]
for backend_class, module_name in backends:
try:
__import__(module_name)
logger.debug(f"Using {module_name} as JSON backend")
return backend_class()
except ImportError:
continue
logger.warning("No JSON backend found, using standard JSON backend")
return StandardJSONBackend()
json_instance = JSON()

View file

@ -0,0 +1,117 @@
from typing import Any, Dict
from unittest.mock import patch
import pytest
from freedium_library.utils.json import (
JSON,
JSONBackend,
OrjsonBackend,
StandardJSONBackend,
UjsonBackend,
)
# Test data
SIMPLE_DICT: Dict[str, Any] = {"key": "value", "number": 42}
COMPLEX_DICT: Dict[str, Any] = {
"string": "hello",
"number": 42,
"float": 3.14,
"bool": True,
"null": None,
"array": [1, 2, 3],
"nested": {"a": 1, "b": 2},
}
UNICODE_DICT: Dict[str, str] = {"unicode": "Hello 世界"}
@pytest.fixture
def json_backends() -> list[JSONBackend]:
return [OrjsonBackend(), UjsonBackend(), StandardJSONBackend()]
def test_backend_interface() -> None:
"""Test that all backends implement the required interface"""
for backend in [OrjsonBackend(), UjsonBackend(), StandardJSONBackend()]:
assert isinstance(backend, JSONBackend)
assert callable(backend.dumps)
assert callable(backend.loads)
assert isinstance(backend.name, str)
assert str(backend).startswith("JSON Backend:")
assert repr(backend).endswith(f"backend='{backend.name}')")
@pytest.mark.parametrize(
"backend", [OrjsonBackend(), UjsonBackend(), StandardJSONBackend()]
)
class TestBackendBehavior:
def test_simple_roundtrip(self, backend: JSONBackend) -> None:
"""Test serialization and deserialization of simple data"""
serialized: str = backend.dumps(SIMPLE_DICT)
deserialized: Dict[str, Any] = backend.loads(serialized)
assert deserialized == SIMPLE_DICT
def test_complex_roundtrip(self, backend: JSONBackend) -> None:
"""Test serialization and deserialization of complex data"""
serialized: str = backend.dumps(COMPLEX_DICT)
deserialized: Dict[str, Any] = backend.loads(serialized)
assert deserialized == COMPLEX_DICT
def test_unicode_handling(self, backend: JSONBackend) -> None:
"""Test proper Unicode handling"""
serialized: str = backend.dumps(UNICODE_DICT)
deserialized: Dict[str, str] = backend.loads(serialized)
assert deserialized == UNICODE_DICT
def test_pretty_printing(self, backend: JSONBackend) -> None:
"""Test pretty printing functionality"""
ugly: str = backend.dumps(COMPLEX_DICT, pretty=False)
pretty: str = backend.dumps(COMPLEX_DICT, pretty=True)
assert len(pretty) > len(ugly)
assert backend.loads(pretty) == backend.loads(ugly)
class TestJSONClass:
def test_backend_selection(self) -> None:
"""Test automatic backend selection"""
json = JSON()
assert json.backend() in ["orjson", "ujson", "json"]
def test_backend_fallback(self) -> None:
"""Test fallback behavior when preferred backends are unavailable"""
JSON._instance = None
JSON._backend = None
with patch.dict("sys.modules", {"orjson": None, "ujson": None}):
json = JSON()
assert json.backend() == "json"
def test_explicit_backend(self) -> None:
"""Test using an explicitly provided backend"""
backend = StandardJSONBackend()
json = JSON(backend=backend)
assert json.backend() == "json"
def test_singleton_behavior(self) -> None:
"""Test that JSON class maintains singleton behavior"""
json1 = JSON()
json2 = JSON()
assert json1._backend is json2._backend
def test_class_methods(self) -> None:
"""Test class-level convenience methods"""
data: Dict[str, Any] = COMPLEX_DICT
serialized: str = JSON.dumps(data)
deserialized: Dict[str, Any] = JSON.loads(serialized)
assert deserialized == data
@pytest.mark.parametrize(
"backend", [OrjsonBackend(), UjsonBackend(), StandardJSONBackend()]
)
def test_error_handling(backend: JSONBackend) -> None:
"""Test error handling for invalid JSON"""
with pytest.raises(Exception):
backend.loads("invalid json")
with pytest.raises(Exception):
backend.dumps(object())

View file

@ -0,0 +1 @@