From fb98541a71aec88e64a10738c1944a735a00caec Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Thu, 23 Apr 2026 23:21:56 +0300 Subject: [PATCH 1/9] =?UTF-8?q?=D0=9D=D0=B0=D1=87=D0=B8=D0=BD=D0=B0=D1=8E?= =?UTF-8?q?=20=D0=BF=D0=B8=D1=81=D0=B0=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 6 +- poetry.lock | 846 +++++++++++++++++++++++++++++++++++- todo.md | 425 ++++++++++++++++++ 3 files changed, 1268 insertions(+), 9 deletions(-) create mode 100644 todo.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 50650cf..60e6c00 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,11 @@ "Bash(grep -E '\\(def |\"\"\"$\\)')", "Bash(\" /Users/n.baryshnikov/Projects/avito_python_api/avito/client.py)", "Bash(rg \"```\" /Users/n.baryshnikov/Projects/avito_python_api/avito/client.py)", - "Bash(grep ``` *)" + "Bash(grep ``` *)", + "Bash(poetry install *)", + "Bash(poetry lock *)", + "Bash(awk -F'|' '{print $4}')", + "Bash(grep -v \"^$\")" ] } } diff --git a/poetry.lock b/poetry.lock index 8d3b410..f0e78bc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,30 +18,231 @@ idna = ">=2.8" [package.extras] trio = ["trio (>=0.32.0)"] +[[package]] +name = "babel" +version = "2.18.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, +] + +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] + +[[package]] +name = "backrefs" +version = "6.2" +description = "A wrapper around re and regex that adds additional back references." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8"}, + {file = "backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be"}, + {file = "backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90"}, + {file = "backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b"}, + {file = "backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7"}, + {file = "backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7"}, + {file = "backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49"}, +] + +[package.extras] +extras = ["regex"] + +[[package]] +name = "bracex" +version = "2.6" +description = "Bash style brace expander." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952"}, + {file = "bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7"}, +] + [[package]] name = "certifi" version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["main", "dev", "docs"] files = [ {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, +] + +[[package]] +name = "click" +version = "8.3.3" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"}, + {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" +groups = ["dev", "docs"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {dev = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -162,6 +363,24 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + [[package]] name = "h11" version = "0.16.0" @@ -227,7 +446,7 @@ version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main", "dev", "docs"] files = [ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, @@ -248,6 +467,24 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "librt" version = "0.9.0" @@ -349,6 +586,283 @@ files = [ {file = "librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d"}, ] +[[package]] +name = "markdown" +version = "3.10.2" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, + {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, +] + +[package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +groups = ["docs"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mike" +version = "2.2.0" +description = "Manage multiple versions of your MkDocs-powered documentation" +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040"}, + {file = "mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e"}, +] + +[package.dependencies] +jinja2 = ">=2.7" +mkdocs = ">=1.0,<2.0" +pyparsing = ">=3.0" +pyyaml = ">=5.1" +pyyaml-env-tag = "*" +verspec = "*" + +[package.extras] +dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] +test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-awesome-pages-plugin" +version = "2.10.1" +description = "An MkDocs plugin that simplifies configuring page titles and their order" +optional = false +python-versions = ">=3.8.1" +groups = ["docs"] +files = [ + {file = "mkdocs_awesome_pages_plugin-2.10.1-py3-none-any.whl", hash = "sha256:c6939dbea37383fc3cf8c0a4e892144ec3d2f8a585e16fdc966b34e7c97042a7"}, + {file = "mkdocs_awesome_pages_plugin-2.10.1.tar.gz", hash = "sha256:cda2cb88c937ada81a4785225f20ef77ce532762f4500120b67a1433c1cdbb2f"}, +] + +[package.dependencies] +mkdocs = ">=1" +natsort = ">=8.1.0" +wcmatch = ">=7" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +description = "An extra command for MkDocs that infers required PyPI packages from `plugins` in mkdocs.yml" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650"}, + {file = "mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.2.2" +description = "Mkdocs Markdown includer plugin." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "mkdocs_include_markdown_plugin-7.2.2-py3-none-any.whl", hash = "sha256:f2ec4487cf32d3e33ca528f9366f20fb9280ded9c8d1630eb2bbda244962dcd1"}, + {file = "mkdocs_include_markdown_plugin-7.2.2.tar.gz", hash = "sha256:f052ccb741eccf498116b826c1d78a2d761c56747372594709441cee0963fbc9"}, +] + +[package.dependencies] +mkdocs = ">=1.4" +wcmatch = "*" + +[package.extras] +cache = ["platformdirs"] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba"}, + {file = "mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69"}, +] + +[package.dependencies] +babel = ">=2.10" +backrefs = ">=5.7.post1" +colorama = ">=0.4" +jinja2 = ">=3.1" +markdown = ">=3.2" +mkdocs = ">=1.6,<2" +mkdocs-material-extensions = ">=1.3" +paginate = ">=0.5" +pygments = ">=2.16" +pymdown-extensions = ">=10.2" +requests = ">=2.30" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4)"] +imaging = ["cairosvg (>=2.6)", "pillow (>=10.2)"] +recommended = ["mkdocs-minify-plugin (>=0.7)", "mkdocs-redirects (>=1.2)", "mkdocs-rss-plugin (>=1.6)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + [[package]] name = "mypy" version = "1.20.1" @@ -429,25 +943,57 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "natsort" +version = "8.4.0" +description = "Simple yet flexible natural sorting in Python." +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"}, + {file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"}, +] + +[package.extras] +fast = ["fastnumbers (>=2.0.0)"] +icu = ["PyICU (>=1.0.0)"] + [[package]] name = "packaging" version = "26.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["dev", "docs"] files = [ {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, ] +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + [[package]] name = "pathspec" version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["dev", "docs"] files = [ {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, @@ -459,6 +1005,18 @@ optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] +[[package]] +name = "platformdirs" +version = "4.9.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, + {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -481,7 +1039,7 @@ version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["dev", "docs"] files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, @@ -490,6 +1048,40 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638"}, + {file = "pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.19.1)"] + +[[package]] +name = "pyparsing" +version = "3.3.2" +description = "pyparsing - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "8.4.2" @@ -512,6 +1104,141 @@ pygments = ">=2.7.2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["docs"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "requests" +version = "2.33.1" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + [[package]] name = "respx" version = "0.22.0" @@ -556,6 +1283,18 @@ files = [ {file = "ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6"}, ] +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["docs"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -568,7 +1307,98 @@ files = [ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "verspec" +version = "0.1.0" +description = "Flexible version handling" +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] + +[package.extras] +test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] + +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcmatch" +version = "10.1" +description = "Wildcard/glob file name matcher." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a"}, + {file = "wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af"}, +] + +[package.dependencies] +bracex = ">=2.1.1" + [metadata] lock-version = "2.1" python-versions = "^3.14" -content-hash = "4671104b283afa43f806f6ba5586c4a3e89c55b29065d6b32e86bf2d29c2df37" +content-hash = "0cca9123ad2deb513c6021da369efe32eedb94e2e1f253c9fbc461d88420faa4" diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..6e05d43 --- /dev/null +++ b/todo.md @@ -0,0 +1,425 @@ +# Документация avito-py на MkDocs Material + +## Context + +SDK `avito-py` покрывает 204 операции Avito API через 11 публичных доменных пакетов (`accounts`, `ads`, `autoteka`, `cpa`, `jobs`, `messenger`, `orders`, `promotion`, `ratings`, `realty`, `tariffs`) и 58 фабричных методов на `AvitoClient`. Число публичных доменов **не хардкодится** — вычисляется как уникальные значения колонки `пакет_sdk` в `docs/avito/inventory.md`, исключая `auth`, `core` и `testing`. + +Сейчас у пользователя есть `README.md` с quickstart и доменными how-to snippet'ами (как требует STYLEGUIDE § Documentation Structure), русские docstring'и в публичном API и `CHANGELOG.md` в корне. Docstring'и не считать готовыми к строгому reference-гейту: перед включением `pydocstyle`/`interrogate` нужен отдельный проход по публичным контрактам, потому что часть docstring'ов сейчас короткая и не покрывает Returns/Raises/идемпотентность по STYLEGUIDE. **Каркас сайта уже создан** (PR 1 в основном реализован) — см. раздел «Текущее состояние». + +STYLEGUIDE § Documentation Structure делает обязательными все четыре режима Diátaxis (tutorials / how-to / reference / explanations); usability_scorecard § 15 выделяет на документацию 7% итогового Score и фиксирует шесть подкритериев с измеримыми процедурами. + +Цель — **стабилизировать существующий каркас** и достроить сайт, **не подменяя README** (доменные how-to snippets в README остаются — это нормативное требование STYLEGUIDE.md:678), а дополняя его режимами, которые в README невозможно компактно уместить: полный reference, длинные how-to с диаграммами, explanations и deploy-версионирование. + +**Измеримые цели**: + +- новичок (P1) доходит от `pip install` до `get_self()` за ≤15 минут (scorecard 1.6); +- опытный разработчик (P2) находит нужный метод без чтения исходников (scorecard 2.*); +- сопровождающий (P3) видит совместимость и deprecation без заглядывания в git-лог (scorecard 18.*); +- scorecard §15 закрыт по всем подпунктам 15.1–15.6 отдельно; агрегат вроде «6% из 7%» не заменяет провал отдельного подпункта; +- P2 может написать consumer-side test поверх SDK через документированный `avito.testing` без приватных полей и без реального HTTP (scorecard 16.1–16.2); +- Diátaxis-матрица 4×N заполнена; каждый публичный домен имеет минимум одну how-to на сайте (в дополнение к snippet'у в README). +- `docs-quality-report` покрывает не только scorecard §15, но и supporting-gates для §16.1–16.4 и §18.1–18.5, потому что production-ready docs здесь завязана на public testing contract и deprecation/CHANGELOG contract, а не только на markdown-страницы. + +Язык — только русский. Визуализация — сбалансированная (Mermaid, admonitions, tabbed code, без кастомной темы). Версионирование — через `mike`. + +## Текущее состояние (уже реализовано) + +PR 1 в основном сделан: + +- `mkdocs.yml` существует, настроен на `docs_dir: docs/site`. +- Группа `docs` в `pyproject.toml` содержит `mkdocs-material`, `mkdocs-awesome-pages-plugin`, `mkdocs-include-markdown-plugin`, `mike`. +- `.github/workflows/docs.yml` собирает сайт и деплоит через `mike`. +- Цели `docs-serve` / `docs-build` в `Makefile`. +- `[tool.poetry.urls].Documentation` указан в метаданных Poetry. +- Структура `docs/site/` с плейсхолдерами четырёх Diátaxis-разделов. +- `docs/site/index.md` — hero + три карточки + Diátaxis-карта. +- `docs/site/tutorials/getting-started.md` — первый рабочий tutorial. +- `docs/site/tutorials/first-promotion.md` — плейсхолдер. +- `docs/site/changelog.md` — include корневого `CHANGELOG.md`. + +**Что сломано в текущем состоянии** (надо починить в PR 1): + +1. `mkdocs build --strict` падает с 8 предупреждениями (strict-mode превращает warning → error): + - Nav-ссылки вида `tutorials/` в `mkdocs.yml` не разрешаются до того, как плагин awesome-pages обработает nav; решение — удалить `nav` из `mkdocs.yml` и завести `docs/site/.pages` с порядком разделов и русскими именами вкладок. + - `docs/site/index.md` содержит ссылку `../avito/inventory.md` — файл вне `docs_dir`; решить добавлением `docs/site/reference/coverage.md` и ссылкой на неё. + - Несколько ссылок из `tutorials/getting-started.md` ведут на страницы, которые ещё не созданы (`how-to/auth-and-config.md`, `reference/client.md`); добавить плейсхолдеры. +2. `avito.testing.__init__` экспортирует только `FakeTransport` и `FakeResponse`, но `fake_transport.py` объявляет в `__all__` также `JsonValue`, `RecordedRequest`, `json_response`, `route_sequence` — нужные в how-to/reference примерах; синхронизировать публичный export. + +## Зафиксированные решения + +### Навигация + +- Material-фичи: `navigation.tabs`, `navigation.sections`, `navigation.indexes`, `navigation.top`, `toc.follow`. +- `nav` удаляется из `mkdocs.yml`; nav управляется файлами `.pages` (awesome-pages plugin). +- Корневой `docs/site/.pages`: + ```yaml + nav: + - Главная: index.md + - Tutorials: tutorials + - How-to: how-to + - Reference: reference + - Explanations: explanations + - Changelog: changelog.md + ``` + +### Код-блоки и аннотации + +`content.code.annotate` **остаётся включённым глобально** — это фича рендеринга, не синтаксис Python. Проблема не в ней, а в аннотационном синтаксисе `# (N)!` внутри Python-блоков: mktestdocs передаёт блок в Python как есть, и `# (1)!` — невалидный Python-комментарий в Material-смысле (хотя технически парсится, маркер-символ может путать инструменты). Поэтому: + +- **Правило**: в `tutorials/*.md` и `how-to/*.md` Python-блоки никогда не содержат аннотационных маркеров `# (N)!`. Это plain fenced code без Material-специфичного синтаксиса. +- `pymdownx.tabbed` тоже не используется в tutorials/how-to. +- В `explanations/` и `reference/` аннотации и вкладки разрешены — mktestdocs там не применяется. + +### Исполняемость примеров (mktestdocs harness) + +`mktestdocs` через `pytest tests/docs/`. Финальная политика: **все fenced code-блоки с меткой `python` или `pycon` в `README.md`, `tutorials/*.md` и `how-to/*.md` исполняются**. Bash, env, mermaid — не исполняются (нет метки `python`). + +Правила классификации примеров: + +- если блок показывает SDK-вызов и помечен как `python`/`pycon`, он обязан выполняться через docs-harness без сети; +- если блок иллюстративный и не должен исполняться, он не имеет метки `python`/`pycon` (`text`, `console`, `bash` и т.п.) и не считается copy-paste примером; +- в `reference/` и `explanations/` Python-блоки либо подключаются к тому же collector'у, либо заменяются на non-executable fence; скрытых непроверяемых SDK-примеров быть не должно; +- реальный HTTP допускается только в ручной TTFC-процедуре с настоящими ключами, а не в CI. + +**Проблема изоляции**: `tutorials/getting-started.md:47` вызывает `AvitoClient.from_env().account().get_self()` — это реальный HTTP-запрос. В CI без API-секретов он упадёт. Базовое решение для PR 3 — `tests/docs/conftest.py` с pytest-фикстурой, которая: +1. Monkeypatches `AvitoClient.from_env()` → возвращает lightweight docs-test facade, повторяющий только публичные методы, используемые в README/tutorials/how-to (`account()`, `ad()`, и т.д.). +2. Facade внутри использует настоящие доменные объекты SDK, созданные поверх `FakeTransport.build()`, чтобы проверялись реальные публичные вызовы доменов без сетевого доступа. +3. FakeTransport скриптован `route_sequence` на типичные ответы (get_self, get_items, и т.д.), покрывающие все README/tutorials/how-to. +4. Env-переменные `AVITO_CLIENT_ID`/`AVITO_CLIENT_SECRET` устанавливаются в фикстуре как заглушки. + +Ограничение harness: monkeypatch только `AvitoClient.from_env()` покрывает tutorial-путь, но не покрывает Python-блоки, где конструируется `AvitoClient(client_id=..., client_secret=...)` или `AvitoClient(AvitoSettings(...))` и затем выполняется SDK-вызов. Зафиксированный контракт: + +- в executable examples сетевые SDK-вызовы идут через `AvitoClient.from_env()`; +- остальные способы инициализации (`AvitoClient(client_id=...)`, `AvitoClient(AvitoSettings(...))`) можно показывать, но без вызова методов, которые идут в transport; +- consumer-testing примеры используют `FakeTransport.as_client()` после добавления публичного testing API в PR 3; +- если документации нужен executable пример с прямым `AvitoClient(...)` и последующим SDK-вызовом, сначала расширяется docs-harness публичным тестовым API; monkeypatch приватных полей запрещён. + +Не использовать хрупкий вариант «создать настоящий `AvitoClient`, потом заменить internals»: у `AvitoClient` нет публичного параметра `transport`, а STYLEGUIDE требует иммутабельности клиента после создания. Если в будущем понадобится полноценный `AvitoClient` с fake transport для docs-тестов, это отдельное публичное/тестовое API-решение, а не monkeypatch приватных полей. + +Это позволяет README/tutorials/how-to показывать реальный API (`from_env()`) для P1-аудитории, и при этом тестировать код без сетевого доступа. Скрипты, которые явно импортируют `FakeTransport` (how-to `testing-with-fake-transport.md`), работают напрямую без monkeypatch. + +**Дизайнерское правило**: каждый новый Python-блок в README/tutorials/how-to обязан работать с harness conftest без сетевых запросов. Если блок требует API-секрет или настоящий transport, это дефект документации, не test-skip. + +### Страница «Покрытие API» (coverage.md) + +`docs/site/reference/coverage.md` — статическая страница внутри `docs_dir`. Она **не ссылается относительными ссылками на `docs/avito/`** (они вне docs_dir и сломают strict-mode). Вместо этого ссылки на Swagger-схемы идут через GitHub blob URL вида `https://github.com///blob/main/docs/avito/api/.json`. Все файлы в `docs/avito/api/` имеют расширение **`.json`**, не `.yaml`. + +**Важно**: `mkdocs.yml:4` сейчас содержит `repo_url: https://github.com/p141592/avito`, при этом `site_url`, badge coverage и локальный каталог проекта указывают на `avito_python_api`. До создания `coverage.md` нужно выбрать один canonical repo URL и синхронно обновить `mkdocs.yml`, Poetry metadata и badges. Если URL окажется неверным, blob-ссылки из coverage.md будут 404. Правило: blob-ссылки в coverage.md хардкодятся с верифицированным URL репозитория и обновляются при смене repo_url в mkdocs.yml; генерировать их динамически из конфига mkdocs не нужно (coverage.md меняется редко). + +### Синхронизация specs ↔ inventory + +`docs/avito/api/*.json` остаётся **единственным authoritative source of truth** по API-контракту; `docs/avito/inventory.md` — это производный индекс для SDK/discovery/doc-generation, а не замена Swagger/OpenAPI-спекам. Поэтому финальный DoD не может опираться только на `inventory-coverage-report.json`. + +Зафиксированный контракт: + +- `scripts/check_spec_inventory_sync.py --output spec-inventory-report.json` сверяет все операции из Swagger/OpenAPI-документов с таблицей `inventory.md`; +- отчёт проверяет как минимум `method + path + документ + раздел`, а не только общее количество строк; +- наличие операции в spec и отсутствие в inventory — дефект inventory; +- наличие операции в inventory без соответствующей spec-записи — дефект inventory или устаревшая запись; +- `coverage.md` может ссылаться на inventory как на удобный индекс, но CI-гейт по полноте строится отдельно через `check_spec_inventory_sync.py`. + +### Deprecated-политика (docs и runtime — разные работы) + +- *Сайт*: `_gen_reference.py` рендерит `!!! warning "Устаревшая операция"` из inventory. Если inventory содержит явное поле `replacement` (см. «Расширение inventory» ниже), генератор добавляет ссылку. Если нет — warning рендерится без replacement; эвристического вывода нет. +- *Runtime*: каждый публичный SDK-символ с `deprecated: да` **обязан** эмитировать `DeprecationWarning` при первом вызове с указанием replacement и целевой версии удаления (STYLEGUIDE § Deprecation Policy). Отсутствие `replacement` в inventory — недостаток inventory, а не повод обходить runtime-требование. +- *Gap-отчёт*: `scripts/check_inventory_coverage.py` (отдельный скрипт — не `_gen_reference.py`) пишет `inventory-coverage-report.json`. Включает `deprecated_without_replacement` для операций без заполненного поля `replacement`. + +Runtime deprecation warnings — это изменение поведения публичного SDK, а не документационная задача. Его нельзя считать частью автогенерации reference. Реализация runtime warnings, тест `tests/contracts/test_deprecation_warnings.py` и запись в `CHANGELOG.md` идут отдельным SDK-contract блоком в PR 2.5/PR 3 до финального DoD. + +### Расширение inventory (prerequisite финального DoD) + +`docs/avito/inventory.md` сейчас не содержит колонок `deprecated_since`, `replacement` и `removal_version`. Без них финальный DoD (`deprecated_without_replacement` пуст и deprecation-период ≥2 minor) **недостижим** — это не дефект плана сайта, это gap в source of truth. В scope PR 2 входит: + +1. Добавить колонки `deprecated_since`, `replacement` и `removal_version` в таблицу операций `docs/avito/inventory.md`. +2. Заполнить значения для всех записей с `deprecated: да`. +3. Обновить `scripts/parse_inventory.py` для разбора новых колонок (`InventoryRow.deprecated_since: str | None`, `InventoryRow.replacement: str | None`, `InventoryRow.removal_version: str | None`). +4. Добавить sanity-check inventory: описание с `(deprecated)` не может иметь `deprecated: нет`; `deprecated: да` не может быть без `deprecated_since`, `replacement`, `removal_version`; `removal_version` должен быть не раньше чем через два minor-релиза после `deprecated_since`. + +До заполнения этих полей финальный DoD не применяется; промежуточные PR мержатся с непустым отчётом. + +### Инструмент проверки ссылок (lychee) + +`lychee` — не Python-зависимость. Для `make docs-check` требует отдельной установки: + +- macOS: `brew install lychee` +- Linux/CI: `cargo binstall lychee` или GitHub Action [`lycheeverse/lychee-action`](https://github.com/lycheeverse/lychee-action) + +Установка документируется в `CONTRIBUTING.md`. Если lychee не найден — `make docs-check` падает с понятным сообщением (не silent skip). В CI lychee запускается через GitHub Action, не через Makefile. + +Конфигурация: `--exclude "avito\.ru"`, `--retry-wait-time 5`, `--max-retries 3`, `--timeout 30`. + +Для локальной работы без lychee доступна цель `make docs-strict` (только `mkdocs build --strict` + Python-gates). + +### Прочие решения + +- **Автогенерация reference**: `mkdocstrings[python]` + `mkdocs-gen-files` + `mkdocs-literate-nav`. Генерируемые файлы (`reference/domains/*.md`, `reference/operations.md`, `reference/SUMMARY.md`) **не коммитятся** — создаются через `mkdocs_gen_files.open()` как виртуальные. +- **Версионирование**: фиксируем конкретную схему `mike`. На `push` в `main` деплоится docs-version `main` с alias `latest` через `mike deploy --push --update-aliases main latest`, затем `mike set-default --push latest`. На `push` тега `vX.Y.Z` деплоится docs-version `X.Y.Z` с alias `stable` через `mike deploy --push --update-aliases X.Y.Z stable`. Root redirect всегда ведёт на alias `latest`; `stable` — это последний релизный docs-snapshot, а не default alias. +- **mkdocstrings-зависимость**: `mkdocstrings = { version = ">=0.27", extras = ["python"] }`. +- **Inventory parser**: `scripts/parse_inventory.py` — reusable, возвращает `list[InventoryRow]` (frozen dataclass). Используется в `_gen_reference.py`, `check_readme_domain_coverage.py`, `check_inventory_coverage.py`, `check_docs_examples.py`, `check_spec_inventory_sync.py`. +- **Разделение ответственности**: `_gen_reference.py` только читает inventory и рендерит страницы. `scripts/check_inventory_coverage.py --output inventory-coverage-report.json` — владелец contract-отчёта. +- **Reference public surface**: generated reference ориентируется на фактическую публичную поверхность: `avito.__all__`, `avito..__all__`, `avito.testing.__all__` и явные страницы для top-level contract (`AvitoClient`, `AvitoSettings`, `AuthSettings`, `PaginatedList`, exceptions). Отдельный скрипт `scripts/check_reference_public_surface.py --output reference-public-report.json` проверяет две вещи: все публичные экспорты попали в reference ровно один раз; internal/private символы вне экспортируемой поверхности не попали в `SUMMARY.md` и discovery-индекс. +- **CI fetch-depth**: `fetch-depth: 0` добавляется в `ci.yml` в PR 3 (нужен для `interrogate` diff-gate против `origin/main`). +- **poetry.lock**: каждый PR, добавляющий зависимости в `pyproject.toml`, коммитит обновлённый `poetry.lock`. Для Poetry 2.x используется `poetry lock` — опции `--no-update` больше нет, а сохранение уже зафиксированных версий является поведением по умолчанию. +- **Контракт README**: `scripts/check_readme_domain_coverage.py` читает домены из inventory через `parse_inventory.py` (не хардкоженный список), выходит с ненулевым кодом при пропущенных; включён в `make docs-strict` и `make docs-check`. +- **pydocstyle**: отдельная цель `make qa-docs`, не в `make lint`. +- **interrogate**: PR 2 — report-only; PR 3 — gate только на изменённые публичные модули. +- **Docstring readiness**: перед `pydocstyle`/`interrogate` привести публичные docstring'и к STYLEGUIDE: возвращаемая SDK-модель, nullable/empty behavior, per-operation overrides, идемпотентность, типовые исключения. Для этого нужен отдельный `scripts/check_public_docstrings.py --output docstring-contract-report.json`: в PR 2 report-only, в PR 3/финальном DoD — strict gate для публичных символов, попадающих в generated reference. До этого `interrogate` может быть только report-only, а reference не считается финально полным. +- **README example sync**: README и tutorial/how-to snippets обязаны отражать реальные публичные сигнатуры текущего SDK. Если public method ушёл с `request=` DTO на flattened keyword-only параметры, старый пример не может жить как “иллюстративный”. Это отдельный docs-contract, проверяемый mktestdocs и review-чек-листом. + +## Структура `docs/site/` + +``` +docs/site/ + .pages # корневой nav для awesome-pages + index.md # hero + три роли-входа (P1/P2/P3) + Diátaxis-карта + tutorials/ + .pages + index.md + getting-started.md # pip install → get_self() — показывает from_env(); тест через harness conftest + first-promotion.md # dry_run=True → dry_run=False + how-to/ + .pages + index.md + auth-and-config.md # placeholder PR 1; содержимое в PR 3 + (остальные рецепты — PR 3) + reference/ + .pages + index.md + coverage.md # PR 1: «Покрытие API» со ссылками на GitHub blob URLs; заменяет битую ../avito/inventory.md + client.md # placeholder PR 1; полный reference — PR 2 + operations.md # PR 2: генерируемая карта operation → SDK method + config.md # PR 2 + domains/ # генерируется _gen_reference.py (не коммитится) + models.md # PR 2 + exceptions.md # PR 2 + pagination.md # PR 2 + testing.md # PR 2 + explanations/ + .pages + index.md + (статьи — PR 3) + changelog.md # include из корневого CHANGELOG.md + assets/ + _gen_reference.py + overrides/ +``` + +## Генерация reference + +`docs/site/assets/_gen_reference.py`: + +1. Импортирует `scripts/parse_inventory.py` для получения `list[InventoryRow]`. +2. Обходит `avito/` исключая internals: `core/transport.py`, `core/retries.py`, `auth/provider.py`, `_env.py`, `__main__.py`. +3. Для каждого публичного пакета создаёт виртуальную страницу `reference/domains/.md` с шапкой (назначение пакета из inventory) и директивой `::: avito.`. Источник публичной поверхности для пакетной страницы — `__all__` экспортируемого пакета, а не простое сканирование всего дерева `avito//`. +4. Создаёт виртуальную `reference/operations.md`: таблица `описание → HTTP method/path → пакет_sdk → доменный_объект → публичный_метод_sdk → deprecated/replacement`. Это основной discovery-индекс для P2. +5. Пишет виртуальный `reference/SUMMARY.md` для `literate-nav`. +6. Для операций с `deprecated: да` вставляет `!!! warning "Устаревшая операция"`. Ссылка на replacement добавляется только если поле `replacement` явно присутствует в `InventoryRow`. Эвристического вывода нет. +7. **Не пишет** в `inventory-coverage-report.json` — это ответственность `scripts/check_inventory_coverage.py`. + +Важно: `scripts/check_inventory_coverage.py` не должен сводиться к простому `hasattr`. Он проверяет связку `пакет_sdk + доменный_объект + публичный_метод_sdk`, special-case `AvitoClient.auth()`, legacy-домены и то, что публичный символ попадает в reference-индекс. Наличие метода без документируемого публичного пути считается gap. + +Все файлы создаются через `mkdocs_gen_files.open()` — **не на диск**, не в git. + +## Опции `mkdocstrings` + +```yaml +handlers: + python: + options: + docstring_style: google + docstring_section_style: table + show_signature_annotations: true + separate_signature: true + merge_init_into_class: true + show_source: false + filters: ["!^_"] + members_order: source + heading_level: 2 +``` + +## Разделение на этапы + +### PR 1 — Стабилизация существующего каркаса + +**Задача**: `mkdocs build --strict` проходит без предупреждений. Deploy проверяется после merge/push в main, потому что PR не публикует GitHub Pages. + +Конкретные изменения: + +- `mkdocs.yml`: удалить секцию `nav`. +- `docs/site/.pages`: создать (см. раздел «Навигация»). +- `docs/site/index.md`: ссылка `../avito/inventory.md` → `reference/coverage.md`. +- `docs/site/reference/coverage.md`: страница «Покрытие API» с таблицей 23 Swagger-документов и GitHub blob URL-ами. +- `mkdocs.yml`, Poetry metadata, badges: синхронизировать canonical repo URL (`avito` vs `avito_python_api`) до добавления blob-ссылок. +- `docs/site/reference/client.md`: placeholder. +- `docs/site/how-to/auth-and-config.md`: placeholder. +- `docs/site/how-to/.pages`: добавить `auth-and-config.md`. +- `docs/site/reference/.pages`: добавить `coverage.md`, `client.md`. +- `avito/testing/__init__.py`: синхронизировать публичный testing-export с `avito.testing.fake_transport.__all__`: `FakeTransport`, `FakeResponse`, `JsonValue`, `RecordedRequest`, `json_response`, `route_sequence`. Если `JsonValue` не должен быть публичным символом, сначала убрать его из обоих `__all__` и reference. + +**Критерий готовности PR 1**: `poetry install --with docs && poetry run mkdocs build --strict` проходит без предупреждений; после merge/push сайт деплоится на GitHub Pages с alias `latest`; TTFC-проверка tutorial проходит вручную. + +### PR 2 — Reference, inventory parser, расширение inventory + +**Prerequisite**: `scripts/parse_inventory.py` — reusable модуль разбора inventory. Возвращает `list[InventoryRow]` (frozen dataclass с полями `deprecated_since: str | None`, `replacement: str | None`, `removal_version: str | None`). + +Конкретные изменения: + +- `docs/avito/inventory.md`: добавить колонки `deprecated_since`, `replacement` и `removal_version` в таблицу операций; заполнить для всех `deprecated: да`. +- `scripts/parse_inventory.py`: реализовать с поддержкой новых колонок. +- `scripts/check_inventory_coverage.py --output `: реализовать; пишет `inventory-coverage-report.json`. Проверяет: каждой inventory-операции соответствует публичный SDK-символ; каждая `deprecated: да` запись имеет `deprecated_since`, `replacement` и `removal_version`; deprecation-период не меньше двух minor-релизов; описание и колонка `deprecated` не противоречат друг другу. В PR 2 работает report-only и не блокирует merge; hard `exit 1` на непустой gap-report включается в PR 3 / финальном DoD. +- `scripts/check_spec_inventory_sync.py --output `: реализовать; пишет `spec-inventory-report.json`. Проверяет: каждая операция из `docs/avito/api/*.json` присутствует в inventory; в inventory нет операций, отсутствующих в spec; совпадают `документ + метод + путь + раздел`. В PR 2 работает report-only и публикуется как CI artifact. +- `scripts/check_readme_domain_coverage.py`: реализовать; домены из inventory. +- `scripts/check_reference_public_surface.py --output `: реализовать; пишет `reference-public-report.json`. Проверяет: все экспорты из `avito.__all__`, `avito..__all__`, `avito.testing.__all__` попадают в reference; лишние internal/private символы не попадают в generated nav/discovery pages. В PR 2 работает report-only и публикуется как CI artifact. +- Docstring readiness audit: сформировать report-only список публичных классов/методов, где docstring не покрывает требования STYLEGUIDE. Реализация — `scripts/check_public_docstrings.py --output ` с проверкой обязательных contract-aspects, а не только наличия docstring. Не блокирует PR 2, но блокирует перевод `interrogate` в gate. +- `pyproject.toml`, группа `docs` — добавить и обновить `poetry.lock`: + ```toml + mkdocstrings = { version = ">=0.27", extras = ["python"] } + mkdocs-gen-files = ">=0.5" + mkdocs-literate-nav = ">=0.6" + ``` +- `mkdocs.yml`: подключить `gen-files`, `literate-nav`, `mkdocstrings[python]`. +- `docs/site/assets/_gen_reference.py`: реализовать (виртуальные файлы). +- `docs/site/reference/`: создать `config.md`, `models.md`, `exceptions.md`, `pagination.md`, `testing.md`; `operations.md` генерируется виртуально из inventory. +- `Makefile`: + ```makefile + docs-strict: + poetry run mkdocs build --strict + poetry run python scripts/check_readme_domain_coverage.py + + docs-check: docs-strict + lychee --exclude "avito\.ru" --retry-wait-time 5 --max-retries 3 --timeout 30 site/ + ``` +- `interrogate` — report-only: CI публикует артефакт `interrogate-report.txt`. Baseline коммитится в `.interrogate-baseline`: + ```json + {"modules": {"avito/accounts/client.py": 92.5, ...}, "generated_at": "", "interrogate_version": ""} + ``` + +**Критерий готовности PR 2**: все пункты STYLEGUIDE § What Constitutes the Public SDK Contract имеют reference-страницу или явно отмеченный docstring gap; deprecated-бейджи рендерятся; `reference/operations.md` строится из inventory; `make docs-strict` проходит; `inventory-coverage-report.json`, `spec-inventory-report.json`, `reference-public-report.json` и `docstring-contract-report.json` публикуются как CI-артефакты; колонки `deprecated_since`/`replacement`/`removal_version` заполнены для всех `deprecated: да` записей. Непустые SDK coverage/spec-sync/reference-public/docstring gaps допустимы только как report-only артефакты PR 2 и должны быть закрыты к финальному DoD. + +### PR 2.5 — Runtime deprecation contract + +**Задача**: синхронизировать runtime-поведение SDK с deprecated-данными inventory. Это публичное SDK-изменение, поэтому оно отделено от генерации сайта. + +Конкретные изменения: + +- Добавить runtime `DeprecationWarning` для каждого публичного SDK-символа с `deprecated: да`, при первом вызове, с replacement и целевой версией удаления. +- Добавить/обновить docstring line у deprecated-символов: replacement и target removal version. +- Добавить `tests/contracts/test_deprecation_warnings.py`, который строит cases из inventory, но проверяет поведение через реальные публичные вызовы/минимальные fake-transport сценарии, а не только наличие атрибута. +- Добавить запись в `CHANGELOG.md` в секцию `Deprecated`; проверить, что CHANGELOG релиза содержит стандартные секции `Added`/`Changed`/`Deprecated`/`Removed`/`Fixed` (пустые секции допустимы только если политика changelog это явно разрешает). + +**Критерий готовности PR 2.5**: `pytest tests/contracts/test_deprecation_warnings.py` зелёный; runtime warnings не дублируются сверх первого вызова; `make test typecheck lint` зелёные. + +### PR 3 — How-to, explanations и quality gates + +Конкретные изменения: + +- `docs/site/how-to/*` — 14 рецептов с фиксированными файлами: + `auth-and-config.md`, `chat-image-upload.md`, `promotion-dry-run.md`, `pagination.md`, + `order-labels.md`, `job-applications.md`, `autoteka-report.md`, `realty-booking.md`, + `cpa-calltracking.md`, `ratings-and-tariffs.md`, `per-operation-overrides.md`, + `idempotency.md`, `testing-with-fake-transport.md`, `diagnostics-and-logging.md`. +- `docs/site/explanations/*` — 8 концептуальных статей с Mermaid: + `architecture.md`, `auth-flow.md`, `transport-and-retries.md`, `error-model.md`, + `pagination-semantics.md`, `dry-run-and-idempotency.md`, `testing-strategy.md`, + `api-coverage-and-deprecations.md`. +- До массового написания how-to: обновить `README.md` и уже существующие tutorial-snippet'ы под реальные публичные сигнатуры текущего SDK. Устаревшие примеры с `request=` DTO там, где сигнатура уже flattened, переписываются, а не помечаются как “illustrative”. +- Перед `testing-with-fake-transport.md`: добавить публичный consumer-testing API. Выбранный контракт: `FakeTransport.as_client(*, user_id: int | None = None, retry_policy: RetryPolicy | None = None) -> AvitoClient`. Он создаёт полностью инициализированный `AvitoClient` поверх fake transport без post-init monkeypatch приватных полей и без публичного параметра `transport` в `AvitoClient.__init__`. `FakeTransport.build()` не используется в пользовательской документации; если он остаётся, он помечается как low-level/internal testing helper или проходит deprecation policy. +- Перед массовым написанием рецептов: реализовать mktestdocs harness на `getting-started.md` и одном how-to, прогнать `pytest tests/docs/`, затем масштабировать на остальные страницы. +- `tests/docs/conftest.py`: mktestdocs harness — monkeypatch `AvitoClient.from_env()` → lightweight docs-test facade поверх настоящих доменных объектов и `FakeTransport.build()`; заглушки env-переменных. Если how-to выполняют сетевые вызовы через прямой `AvitoClient(...)`, сначала принять отдельное публичное тестовое API для fake transport или переписать пример так, чтобы сетевой вызов выполнялся через `from_env()`. +- `tests/docs/test_docs_harness_surface.py`: проверяет, что docs-test facade не изобретает собственный API: имена фабрик/методов и callable-сигнатуры, используемые harness, совпадают с реальными публичными сигнатурами `AvitoClient` и соответствующих доменных объектов. +- `tests/docs/test_markdown_examples.py`: pytest-тест, который вызывает mktestdocs для `README.md`, `docs/site/tutorials/*.md` и `docs/site/how-to/*.md`; одного `conftest.py` недостаточно для запуска markdown-примеров. +- `tests/docs/test_no_placeholders.py`: падает, если production docs содержат `Раздел в разработке`, `placeholder`, `плейсхолдер`, `TODO`, `TBD`, `coming soon`. +- `scripts/check_docs_examples.py`: проверяет, что SDK-примеры в `reference/` и `explanations/` либо исполняются тем же collector'ом, либо не помечены как `python`/`pycon`. +- `pyproject.toml`, группа `docs` — добавить и обновить `poetry.lock`: + ```toml + mktestdocs = ">=0.2" + interrogate = ">=1.7" + pydocstyle = { version = ">=6.3", extras = ["toml"] } + ``` +- `mktestdocs` через `pytest tests/docs/`: все `python`/`pycon` блоки в README/tutorials/how-to. Включается в `make docs-strict`. +- `pydocstyle` с профилем Google — `make qa-docs`, **не** `make lint`. +- `interrogate` gate — diff против `origin/main`; per-module vs baseline. `ci.yml`: `fetch-depth: 0`. +- `CONTRIBUTING.md`: инструкция по установке lychee (`brew install lychee` / `cargo binstall lychee`); review-чек-лист README domain coverage. +- `.github/pull_request_template.md`: чек-лист coverage. +- `.github/workflows/docs.yml`: шаг `lycheeverse/lychee-action` (не вызов `make docs-check`); на `push` в `main` выполняются `mike deploy --push --update-aliases main latest` и `mike set-default --push latest`; на `push` тега `v*` выполняется `mike deploy --push --update-aliases stable`, где `` берётся из тега без `v`. +- `.github/workflows/ci.yml`: добавить `make docs-strict` в пайплайн; `fetch-depth: 0`. +- `Makefile`: в PR 3 расширить `docs-strict`, добавив `poetry run pytest tests/docs/`. В PR 2 snippet `docs-strict` ещё не включает mktestdocs. +- `docs-quality-report.md` или CI artifact `docs-quality-report.json`: фиксирует Diátaxis-матрицу, прохождение 15.1–15.6, TTFC-замер, README/domain coverage, markdown examples, lychee, inventory coverage, spec↔inventory sync, reference public surface, docstring readiness, а также supporting-gates для scorecard §16 и §18. + +**Критерий готовности PR 3**: Diátaxis-матрица 4×N; каждый публичный домен (из inventory, кроме auth/core/testing) имеет ≥1 how-to; README/tutorials/how-to синхронизированы с реальными публичными сигнатурами SDK; все `python`/`pycon` блоки в README/tutorials/how-to исполняются через mktestdocs с harness conftest; docs-harness surface проверен отдельным тестом; SDK-примеры в reference/explanations либо исполняются, либо не помечены как executable; `make docs-strict`, `make qa-docs` и CI lychee-step проходят; `mike list` показывает как минимум `main [latest]` и текущий релиз `[stable]`; scorecard §15.1–15.6 закрыт по каждому подпункту, а `docs-quality-report` показывает supporting-gates для §16 и §18. + +## Риски и их нейтрализация + +| Риск | Нейтрализация | +|---|---| +| mktestdocs падает на `AvitoClient.from_env()` в CI | `tests/docs/conftest.py` monkeypatches from_env → docs-test facade поверх FakeTransport; реальных API-вызовов нет | +| mktestdocs пропускает прямой `AvitoClient(...)` и уходит в сеть | Использовать зафиксированный контракт: executable network calls только через `from_env()`, consumer-testing через `FakeTransport.as_client()`, прямой `AvitoClient(...)` без transport-вызова | +| README содержит SDK-snippet'ы, которые не покрыты docs-harness | Включить `README.md` в `tests/docs/test_markdown_examples.py`; переписать или переклассифицировать каждый non-executable блок | +| Annotation-маркеры `# (N)!` ломают Python-блоки | Правило: в tutorials/how-to нет аннотационного синтаксиса; `content.code.annotate` остаётся глобально включённым (это не источник проблемы) | +| `coverage.md` ссылается на файлы вне `docs_dir` | Только GitHub blob URLs; нет относительных ссылок на `docs/avito/` | +| Inventory расходится со Swagger/OpenAPI-спеками | `check_spec_inventory_sync.py` сравнивает `docs/avito/api/*.json` с `inventory.md`; report-only в PR 2, strict в финальном DoD | +| lychee не установлен локально | `make docs-strict` без lychee; `make docs-check` документирует зависимость; в CI — GitHub Action | +| Финальный DoD по deprecated недостижим без inventory | В scope PR 2: добавить колонки `deprecated_since`/`replacement`/`removal_version`; финальный DoD применяется только после их заполнения | +| Inventory содержит противоречивые deprecated-данные | `check_inventory_coverage.py` проверяет `description` vs `deprecated`, обязательные `deprecated_since`/`replacement`/`removal_version` и deprecation-период | +| Runtime deprecated warnings смешиваются с docs-задачей | Выделить PR 2.5 SDK-contract: warnings, docstrings, tests, CHANGELOG | +| `_gen_reference.py` становится владельцем contract-логики | `check_inventory_coverage.py` владеет отчётом; генератор только рендерит | +| `check_inventory_coverage.py` превращается в `hasattr`-проверку | Проверять связку из inventory, special-case auth/legacy и попадание символа в reference-индекс | +| Generated reference случайно протекает internal/private surface | `check_reference_public_surface.py` сверяет reference с `__all__`-экспортами и top-level contract | +| `poetry.lock` устаревает | Каждый PR с новыми deps коммитит обновлённый lock (`poetry lock` для Poetry 2.x) | +| interrogate diff требует git history | `fetch-depth: 0` в ci.yml (PR 3) | +| lychee шумит на нестабильных хостах | `--exclude "avito\.ru"`, retry 3, timeout 30с | +| `repo_url` расходится с GitHub Pages/coverage badge | В PR 1 выбрать canonical repo и синхронизировать `mkdocs.yml`, Poetry metadata, badges и blob-ссылки | +| В production docs остаются плейсхолдеры | `tests/docs/test_no_placeholders.py` и финальный `rg`-gate на `Раздел в разработке|placeholder|плейсхолдер|TODO|TBD|coming soon` | +| README/snippet'ы отстают от реальных public signatures | Обязательная синхронизация примеров в PR 3 + mktestdocs + review-чек-лист | +| Docs-harness начинает жить отдельно от реального API | `tests/docs/test_docs_harness_surface.py` сверяет facade с `AvitoClient` и доменными public methods | + +## Реиспользуемые артефакты + +- `avito/testing/__init__.py` (после PR 1: `FakeTransport`, `FakeResponse`, `JsonValue`, `RecordedRequest`, `json_response`, `route_sequence`; после PR 3: `FakeTransport.as_client()`) — harness conftest, how-to, reference. +- `avito/core/exceptions.py` — reference `exceptions.md`, explanation `error-model.md`. +- `avito/core/pagination.py:PaginatedList` — reference `pagination.md`, explanation `pagination-semantics.md`. +- `avito/core/serialization.py:SerializableModel` — reference `models.md`. +- `docs/avito/inventory.md` — парсится через `parse_inventory.py`; источник доменов, deprecated-статусов, `deprecated_since`, `replacement`, `removal_version`. +- Доменные `client.py` с публичными docstring'ами — автопарсятся `mkdocstrings`; до финального DoD они проходят docstring readiness audit по STYLEGUIDE. +- `CHANGELOG.md` — включается через `mkdocs-include-markdown-plugin`. + +## Definition of Done (итоговая) + +- Все PR 1, PR 2, PR 2.5 и PR 3 смержены. +- `mkdocs build --strict` — без предупреждений. +- CI lychee-step — ноль битых ссылок. +- Все `python`/`pycon` блоки в README/tutorials/how-to исполняются через mktestdocs с harness conftest в `pytest tests/docs/`. +- SDK-примеры в reference/explanations либо исполняются тем же collector'ом, либо не помечены как executable Python. +- `interrogate` baseline зафиксирован; gate проходит для изменённых публичных модулей. +- `make qa-docs` зелёный после закрытия docstring readiness gaps. +- Deprecated-статусы в reference совпадают с inventory; runtime `DeprecationWarning` реализован для deprecated SDK-символов; `test_deprecation_warnings` зелёный; `deprecated_since`, `replacement` и `removal_version` заполнены для всех `deprecated: да` записей; deprecation-период не меньше двух minor-релизов. +- `inventory-coverage-report.json` пуст. +- `spec-inventory-report.json` пуст. +- `reference-public-report.json` пуст. +- `docs-quality-report` опубликован как CI artifact и показывает 15.1–15.6 без пропусков, а также supporting-gates для scorecard §16 и §18. +- Docstring readiness gaps закрыты для публичных контрактов, попадающих в generated reference. +- Diátaxis-матрица 4×N; каждый публичный домен (кроме auth/core/testing) имеет ≥1 how-to; `make docs-strict` проходит полностью. +- Reference `operations.md` даёт карту всех inventory operations к публичным SDK-методам. +- Reference `testing.md` и how-to `testing-with-fake-transport.md` покрывают все аспекты public testing contract: scripting responses, call inspection, transport-level errors, `Retry-After`, `as_client()` consumer test. +- Каждый пункт STYLEGUIDE § What Constitutes the Public SDK Contract покрыт reference-страницей. +- README/tutorials/how-to snippet'ы соответствуют актуальным публичным сигнатурам SDK; устаревших примеров с pre-refactor `request=` DTO в flattened-methods не осталось. +- В production docs нет плейсхолдеров: `Раздел в разработке`, `placeholder`, `плейсхолдер`, `TODO`, `TBD`, `coming soon`. +- `mike list` показывает `main [latest]` и как минимум один релизный docs-version с alias `stable`; root redirect ведёт на `latest`. + +## Verification + +1. `poetry install --with docs` — зависимости встают, lock актуален. +2. `make docs-serve` — локальный сайт, четыре Diátaxis-вкладки. +3. `make docs-strict` — после PR 2: `mkdocs build --strict` + `check_readme_domain_coverage.py`; после PR 3 дополнительно mktestdocs через `pytest tests/docs/` и placeholder-gate. +4. `make docs-check` — дополнительно lychee (требует `brew install lychee`). +5. `make qa-docs` — `pydocstyle` с профилем Google. +6. `poetry run pytest tests/docs/` — исполняет README/tutorials/how-to snippets и проверяет отсутствие плейсхолдеров. +7. TTFC-процедура: чистый venv, `pip install avito-py`, tutorial, засечь время до реального `get_self()` с настоящими ключами. +8. `pytest tests/contracts/test_deprecation_warnings.py` — для каждого SDK-символа с `deprecated: да`. +9. `python scripts/check_inventory_coverage.py --strict --output inventory-coverage-report.json` — exit 0. +10. `python scripts/check_spec_inventory_sync.py --strict --output spec-inventory-report.json` — exit 0. +11. `python scripts/check_reference_public_surface.py --strict --output reference-public-report.json` — exit 0. +12. `python scripts/check_public_docstrings.py --strict --output docstring-contract-report.json` — exit 0 после закрытия gaps. +13. `rg -n "Раздел в разработке|placeholder|плейсхолдер|TODO|TBD|coming soon" docs/site README.md` — пустой вывод для production docs. +14. CI: PR с битой ссылкой → lychee-step падает; PR с пониженным coverage → interrogate падает. +15. Push в `main` → `mike deploy --push --update-aliases main latest` + `mike set-default --push latest`; push тега `v*` → `mike deploy --push --update-aliases stable`; `mike list` показывает оба alias. From c4df5691088fb929f7c850cd8358645e2310b24e Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Thu, 23 Apr 2026 23:59:32 +0300 Subject: [PATCH 2/9] =?UTF-8?q?=D0=9D=D0=B0=D1=87=D0=B8=D0=BD=D0=B0=D1=8E?= =?UTF-8?q?=20=D0=BF=D0=B8=D1=81=D0=B0=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todo.md | 263 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 209 insertions(+), 54 deletions(-) diff --git a/todo.md b/todo.md index 6e05d43..a8064b0 100644 --- a/todo.md +++ b/todo.md @@ -15,9 +15,13 @@ STYLEGUIDE § Documentation Structure делает обязательными в - новичок (P1) доходит от `pip install` до `get_self()` за ≤15 минут (scorecard 1.6); - опытный разработчик (P2) находит нужный метод без чтения исходников (scorecard 2.*); - сопровождающий (P3) видит совместимость и deprecation без заглядывания в git-лог (scorecard 18.*); -- scorecard §15 закрыт по всем подпунктам 15.1–15.6 отдельно; агрегат вроде «6% из 7%» не заменяет провал отдельного подпункта; +- `debug_info()` документирован в reference (`client.md`) и покрыт supporting-gate `7.3_debug_info_safe_by_default` в `docs-quality-report.json`. +- `reference/exceptions.md` документирует публичные атрибуты ошибок (`operation`, `status`, `request_id`, `attempt`, `method`, `endpoint`), и `check_public_docstrings.py` проверяет, что методы, поднимающие исключения, описывают доступные поля. +- `reference/enums.md` генерируется автоматически как индекс всех публичных `Enum` из `avito..__all__`; `check_reference_public_surface.py` проверяет полноту. +- `explanations/security-and-redaction.md` фиксирует security-модель SDK: редакция секретов в логах/исключениях/сериализации и публичные гарантии. +- scorecard §15.1–15.6 закрыт отдельно; scorecard §12 (async/sync) явно помечен как disabled, вес перераспределён в `disabled_criteria` поле `docs-quality-report.json`; все `supporting_gates.*` имеют `grade ∈ {0, 0.25, 0.5, 0.75, 1.0}` и non-null `evidence` — `null` считается провалом гейта. - P2 может написать consumer-side test поверх SDK через документированный `avito.testing` без приватных полей и без реального HTTP (scorecard 16.1–16.2); -- Diátaxis-матрица 4×N заполнена; каждый публичный домен имеет минимум одну how-to на сайте (в дополнение к snippet'у в README). +- Diátaxis-матрица 4×N заполнена; каждый публичный домен имеет минимум одну how-to на сайте (в дополнение к snippet'у в README); явный маппинг «домен → файл» зафиксирован в `docs-quality-report.json`. - `docs-quality-report` покрывает не только scorecard §15, но и supporting-gates для §16.1–16.4 и §18.1–18.5, потому что production-ready docs здесь завязана на public testing contract и deprecation/CHANGELOG contract, а не только на markdown-страницы. Язык — только русский. Визуализация — сбалансированная (Mermaid, admonitions, tabbed code, без кастомной темы). Версионирование — через `mike`. @@ -44,6 +48,7 @@ PR 1 в основном сделан: - `docs/site/index.md` содержит ссылку `../avito/inventory.md` — файл вне `docs_dir`; решить добавлением `docs/site/reference/coverage.md` и ссылкой на неё. - Несколько ссылок из `tutorials/getting-started.md` ведут на страницы, которые ещё не созданы (`how-to/auth-and-config.md`, `reference/client.md`); добавить плейсхолдеры. 2. `avito.testing.__init__` экспортирует только `FakeTransport` и `FakeResponse`, но `fake_transport.py` объявляет в `__all__` также `JsonValue`, `RecordedRequest`, `json_response`, `route_sequence` — нужные в how-to/reference примерах; синхронизировать публичный export. +3. `mkdocs.yml` не верифицирован на поддержку mermaid через `pymdownx.superfences.custom_fences`; если конфигурации нет, добавить в PR 1. ## Зафиксированные решения @@ -100,6 +105,8 @@ PR 1 в основном сделан: **Дизайнерское правило**: каждый новый Python-блок в README/tutorials/how-to обязан работать с harness conftest без сетевых запросов. Если блок требует API-секрет или настоящий transport, это дефект документации, не test-skip. +`scripts/check_docs_examples.py` публикует `reference-explanation-examples-report.json`; в PR 3 gate включается strict. Поле `reference_explanation_examples_gaps` добавляется в `docs-quality-report.json`. + ### Страница «Покрытие API» (coverage.md) `docs/site/reference/coverage.md` — статическая страница внутри `docs_dir`. Она **не ссылается относительными ссылками на `docs/avito/`** (они вне docs_dir и сломают strict-mode). Вместо этого ссылки на Swagger-схемы идут через GitHub blob URL вида `https://github.com///blob/main/docs/avito/api/.json`. Все файлы в `docs/avito/api/` имеют расширение **`.json`**, не `.yaml`. @@ -126,6 +133,35 @@ PR 1 в основном сделан: Runtime deprecation warnings — это изменение поведения публичного SDK, а не документационная задача. Его нельзя считать частью автогенерации reference. Реализация runtime warnings, тест `tests/contracts/test_deprecation_warnings.py` и запись в `CHANGELOG.md` идут отдельным SDK-contract блоком в PR 2.5/PR 3 до финального DoD. +PR 2.5 мержится до PR 2b. Причина: deprecated-admonition в reference не должен появляться раньше, чем runtime `DeprecationWarning` эмитируется по вызову символа. + +### Per-operation overrides — канонический набор + +Канонический набор overrides фиксируется в `reference/config.md` как таблица «тип операции → разрешённые overrides»: + +- read / list / probe: `timeout`, `retries`; +- write с `dry_run=False`: `timeout`, `retries`, `idempotency_key`; +- write с `dry_run=True`: `timeout`; +- pagination-чтение: `timeout`, `retries`, `page_size`. + +`check_public_docstrings.py` сверяет реальные override-параметры с этим списком; расхождение — docstring gap. + +### Exception metadata contract + +Каждый подкласс `AvitoError` обязан в `reference/exceptions.md` документировать поля: `operation`, `status`, `request_id`, `attempt`, `method`, `endpoint`. `check_public_docstrings.py` добавляет шестой обязательный аспект — «documented raised exceptions include metadata field list». + +### Security surface docs + +`explanations/security-and-redaction.md` покрывает: (1) что SDK гарантирует в логах и ошибках по STYLEGUIDE §Logging/§Errors, (2) контракт `debug_info()`, (3) как consumer-код не должен логировать результаты `to_dict()` без фильтрации. + +### Scorecard §12 disabled + +SDK синхронный (STYLEGUIDE § HTTP and Transport Layer). Scorecard §12 (async/sync parity) отключён; его 3% веса перераспределяются пропорционально между §2 (+0.24%), §4 (+0.30%), §5 (+0.18%), §6 (+0.24%), §8 (+0.21%), §13 (+0.12%), §15 (+0.21%), §16 (+0.15%), §18 (+0.15%), остальным — согласно формуле Σ weight = 100%. Зафиксировано в `disabled_criteria` поле `docs-quality-report.json`. + +### TTFC runbook + +TTFC-замер выполняется вручную ответственным мейнтейнером перед каждым релизом (и перед финальным DoD). Процедура фиксируется в `CONTRIBUTING.md` раздел «TTFC measurement». Результат пишется в поле `ttfc_minutes` текущего релизного `docs-quality-report.json`. + ### Расширение inventory (prerequisite финального DoD) `docs/avito/inventory.md` сейчас не содержит колонок `deprecated_since`, `replacement` и `removal_version`. Без них финальный DoD (`deprecated_without_replacement` пуст и deprecation-период ≥2 minor) **недостижим** — это не дефект плана сайта, это gap в source of truth. В scope PR 2 входит: @@ -153,17 +189,18 @@ Runtime deprecation warnings — это изменение поведения п ### Прочие решения - **Автогенерация reference**: `mkdocstrings[python]` + `mkdocs-gen-files` + `mkdocs-literate-nav`. Генерируемые файлы (`reference/domains/*.md`, `reference/operations.md`, `reference/SUMMARY.md`) **не коммитятся** — создаются через `mkdocs_gen_files.open()` как виртуальные. +- **Mermaid**: `mkdocs.yml` включает `pymdownx.superfences` с `custom_fences` для `name: mermaid` и `class: mermaid`. В PR 1 каркас верифицируется рендером одной mermaid-диаграммы в `explanations/architecture.md` (минимальный placeholder до PR 3). - **Версионирование**: фиксируем конкретную схему `mike`. На `push` в `main` деплоится docs-version `main` с alias `latest` через `mike deploy --push --update-aliases main latest`, затем `mike set-default --push latest`. На `push` тега `vX.Y.Z` деплоится docs-version `X.Y.Z` с alias `stable` через `mike deploy --push --update-aliases X.Y.Z stable`. Root redirect всегда ведёт на alias `latest`; `stable` — это последний релизный docs-snapshot, а не default alias. - **mkdocstrings-зависимость**: `mkdocstrings = { version = ">=0.27", extras = ["python"] }`. - **Inventory parser**: `scripts/parse_inventory.py` — reusable, возвращает `list[InventoryRow]` (frozen dataclass). Используется в `_gen_reference.py`, `check_readme_domain_coverage.py`, `check_inventory_coverage.py`, `check_docs_examples.py`, `check_spec_inventory_sync.py`. - **Разделение ответственности**: `_gen_reference.py` только читает inventory и рендерит страницы. `scripts/check_inventory_coverage.py --output inventory-coverage-report.json` — владелец contract-отчёта. -- **Reference public surface**: generated reference ориентируется на фактическую публичную поверхность: `avito.__all__`, `avito..__all__`, `avito.testing.__all__` и явные страницы для top-level contract (`AvitoClient`, `AvitoSettings`, `AuthSettings`, `PaginatedList`, exceptions). Отдельный скрипт `scripts/check_reference_public_surface.py --output reference-public-report.json` проверяет две вещи: все публичные экспорты попали в reference ровно один раз; internal/private символы вне экспортируемой поверхности не попали в `SUMMARY.md` и discovery-индекс. +- **Reference public surface**: generated reference ориентируется на фактическую публичную поверхность: `avito.__all__`, `avito..__all__`, `avito.testing.__all__`, все публичные `Enum` из доменных `__all__` и явные страницы для top-level contract (`AvitoClient`, `AvitoClient.debug_info`, `AvitoSettings`, `AuthSettings`, `PaginatedList`, exceptions). Отдельный скрипт `scripts/check_reference_public_surface.py --output reference-public-report.json` проверяет две вещи: все публичные экспорты попали в reference ровно один раз; internal/private символы вне экспортируемой поверхности не попали в `SUMMARY.md` и discovery-индекс. - **CI fetch-depth**: `fetch-depth: 0` добавляется в `ci.yml` в PR 3 (нужен для `interrogate` diff-gate против `origin/main`). - **poetry.lock**: каждый PR, добавляющий зависимости в `pyproject.toml`, коммитит обновлённый `poetry.lock`. Для Poetry 2.x используется `poetry lock` — опции `--no-update` больше нет, а сохранение уже зафиксированных версий является поведением по умолчанию. - **Контракт README**: `scripts/check_readme_domain_coverage.py` читает домены из inventory через `parse_inventory.py` (не хардкоженный список), выходит с ненулевым кодом при пропущенных; включён в `make docs-strict` и `make docs-check`. - **pydocstyle**: отдельная цель `make qa-docs`, не в `make lint`. -- **interrogate**: PR 2 — report-only; PR 3 — gate только на изменённые публичные модули. -- **Docstring readiness**: перед `pydocstyle`/`interrogate` привести публичные docstring'и к STYLEGUIDE: возвращаемая SDK-модель, nullable/empty behavior, per-operation overrides, идемпотентность, типовые исключения. Для этого нужен отдельный `scripts/check_public_docstrings.py --output docstring-contract-report.json`: в PR 2 report-only, в PR 3/финальном DoD — strict gate для публичных символов, попадающих в generated reference. До этого `interrogate` может быть только report-only, а reference не считается финально полным. +- **interrogate**: PR 2b — report-only; PR 3 — gate только на изменённые публичные модули. +- **Docstring readiness**: перед `pydocstyle`/`interrogate` привести публичные docstring'и к STYLEGUIDE: возвращаемая SDK-модель, nullable/empty behavior, per-operation overrides из канонического списка, идемпотентность, типовые исключения с metadata fields, поведение `dry_run=True` для write-методов. Для этого нужен отдельный `scripts/check_public_docstrings.py --output docstring-contract-report.json`: в PR 2 report-only, в PR 3/финальном DoD — strict gate для публичных символов, попадающих в generated reference. До этого `interrogate` может быть только report-only, а reference не считается финально полным. - **README example sync**: README и tutorial/how-to snippets обязаны отражать реальные публичные сигнатуры текущего SDK. Если public method ушёл с `request=` DTO на flattened keyword-only параметры, старый пример не может жить как “иллюстративный”. Это отдельный docs-contract, проверяемый mktestdocs и review-чек-листом. ## Структура `docs/site/` @@ -176,7 +213,7 @@ docs/site/ .pages index.md getting-started.md # pip install → get_self() — показывает from_env(); тест через harness conftest - first-promotion.md # dry_run=True → dry_run=False + first-promotion.md # placeholder PR 1; содержимое — PR 3 how-to/ .pages index.md @@ -186,17 +223,20 @@ docs/site/ .pages index.md coverage.md # PR 1: «Покрытие API» со ссылками на GitHub blob URLs; заменяет битую ../avito/inventory.md - client.md # placeholder PR 1; полный reference — PR 2 - operations.md # PR 2: генерируемая карта operation → SDK method - config.md # PR 2 + client.md # placeholder PR 1; полный reference включая debug_info() — PR 2b + operations.md # PR 2b: генерируемая карта operation → SDK method + config.md # PR 2b: AvitoSettings, AuthSettings, user_agent_suffix, env priority domains/ # генерируется _gen_reference.py (не коммитится) - models.md # PR 2 - exceptions.md # PR 2 - pagination.md # PR 2 - testing.md # PR 2 + enums.md # PR 2b: генерируется из avito..__all__ + models.md # PR 2b + exceptions.md # PR 2b + pagination.md # PR 2b + testing.md # PR 2b explanations/ .pages index.md + architecture.md # PR 1: placeholder с mermaid smoke-test; содержимое — PR 3 + security-and-redaction.md # PR 3: security-модель (статьи — PR 3) changelog.md # include из корневого CHANGELOG.md assets/ @@ -210,11 +250,13 @@ docs/site/ 1. Импортирует `scripts/parse_inventory.py` для получения `list[InventoryRow]`. 2. Обходит `avito/` исключая internals: `core/transport.py`, `core/retries.py`, `auth/provider.py`, `_env.py`, `__main__.py`. -3. Для каждого публичного пакета создаёт виртуальную страницу `reference/domains/.md` с шапкой (назначение пакета из inventory) и директивой `::: avito.`. Источник публичной поверхности для пакетной страницы — `__all__` экспортируемого пакета, а не простое сканирование всего дерева `avito//`. +3. Для каждого публичного пакета создаёт виртуальную страницу `reference/domains/.md` с шапкой (назначение пакета из inventory) и директивой `::: avito.`. Источник публичной поверхности для пакетной страницы — `__all__` экспортируемого пакета, а не простое сканирование всего дерева `avito//`. В отдельной таблице раздела перечисляются `Enum` этого домена с ссылками на `reference/enums.md`. 4. Создаёт виртуальную `reference/operations.md`: таблица `описание → HTTP method/path → пакет_sdk → доменный_объект → публичный_метод_sdk → deprecated/replacement`. Это основной discovery-индекс для P2. 5. Пишет виртуальный `reference/SUMMARY.md` для `literate-nav`. 6. Для операций с `deprecated: да` вставляет `!!! warning "Устаревшая операция"`. Ссылка на replacement добавляется только если поле `replacement` явно присутствует в `InventoryRow`. Эвристического вывода нет. 7. **Не пишет** в `inventory-coverage-report.json` — это ответственность `scripts/check_inventory_coverage.py`. +8. Создаёт виртуальную `reference/enums.md`: индекс всех публичных `Enum` из всех `avito..__all__`, разбитый по доменам, с директивой `::: avito..` для каждого. +9. На странице `reference/client.md` директивой `::: avito.AvitoClient.debug_info` отдельно раскрывает `debug_info()`; отсутствие символа — hard error генератора. Важно: `scripts/check_inventory_coverage.py` не должен сводиться к простому `hasattr`. Он проверяет связку `пакет_sdk + доменный_объект + публичный_метод_sdk`, special-case `AvitoClient.auth()`, legacy-домены и то, что публичный символ попадает в reference-индекс. Наличие метода без документируемого публичного пути считается gap. @@ -246,31 +288,56 @@ handlers: Конкретные изменения: - `mkdocs.yml`: удалить секцию `nav`. +- `mkdocs.yml`: верифицировать `pymdownx.superfences` с mermaid `custom_fence`; включить минимальную mermaid-диаграмму в `explanations/architecture.md` placeholder для смоук-теста рендера. - `docs/site/.pages`: создать (см. раздел «Навигация»). - `docs/site/index.md`: ссылка `../avito/inventory.md` → `reference/coverage.md`. - `docs/site/reference/coverage.md`: страница «Покрытие API» с таблицей 23 Swagger-документов и GitHub blob URL-ами. - `mkdocs.yml`, Poetry metadata, badges: синхронизировать canonical repo URL (`avito` vs `avito_python_api`) до добавления blob-ссылок. +- `docs/site/explanations/`: создать placeholder `architecture.md` с одной mermaid-блок-схемой, чтобы mermaid-рендер был протестирован в PR 1. +- `docs/site/explanations/.pages`: добавить `architecture.md`. - `docs/site/reference/client.md`: placeholder. - `docs/site/how-to/auth-and-config.md`: placeholder. - `docs/site/how-to/.pages`: добавить `auth-and-config.md`. - `docs/site/reference/.pages`: добавить `coverage.md`, `client.md`. - `avito/testing/__init__.py`: синхронизировать публичный testing-export с `avito.testing.fake_transport.__all__`: `FakeTransport`, `FakeResponse`, `JsonValue`, `RecordedRequest`, `json_response`, `route_sequence`. Если `JsonValue` не должен быть публичным символом, сначала убрать его из обоих `__all__` и reference. -**Критерий готовности PR 1**: `poetry install --with docs && poetry run mkdocs build --strict` проходит без предупреждений; после merge/push сайт деплоится на GitHub Pages с alias `latest`; TTFC-проверка tutorial проходит вручную. - -### PR 2 — Reference, inventory parser, расширение inventory +**Критерий готовности PR 1**: `poetry install --with docs && poetry run mkdocs build --strict` проходит без предупреждений; mermaid-блок в `architecture.md` рендерится в `site/` без предупреждений strict-mode; после merge/push сайт деплоится на GitHub Pages с alias `latest`; TTFC-проверка tutorial проходит вручную. -**Prerequisite**: `scripts/parse_inventory.py` — reusable модуль разбора inventory. Возвращает `list[InventoryRow]` (frozen dataclass с полями `deprecated_since: str | None`, `replacement: str | None`, `removal_version: str | None`). +### PR 2a — Inventory, contract-скрипты Конкретные изменения: - `docs/avito/inventory.md`: добавить колонки `deprecated_since`, `replacement` и `removal_version` в таблицу операций; заполнить для всех `deprecated: да`. -- `scripts/parse_inventory.py`: реализовать с поддержкой новых колонок. -- `scripts/check_inventory_coverage.py --output `: реализовать; пишет `inventory-coverage-report.json`. Проверяет: каждой inventory-операции соответствует публичный SDK-символ; каждая `deprecated: да` запись имеет `deprecated_since`, `replacement` и `removal_version`; deprecation-период не меньше двух minor-релизов; описание и колонка `deprecated` не противоречат друг другу. В PR 2 работает report-only и не блокирует merge; hard `exit 1` на непустой gap-report включается в PR 3 / финальном DoD. -- `scripts/check_spec_inventory_sync.py --output `: реализовать; пишет `spec-inventory-report.json`. Проверяет: каждая операция из `docs/avito/api/*.json` присутствует в inventory; в inventory нет операций, отсутствующих в spec; совпадают `документ + метод + путь + раздел`. В PR 2 работает report-only и публикуется как CI artifact. +- `scripts/parse_inventory.py`: реализовать с поддержкой новых колонок; возвращает `list[InventoryRow]` (frozen dataclass с полями `deprecated_since: str | None`, `replacement: str | None`, `removal_version: str | None`). +- `scripts/check_inventory_coverage.py --output `: реализовать; пишет `inventory-coverage-report.json`. Проверяет: каждой inventory-операции соответствует публичный SDK-символ; каждая `deprecated: да` запись имеет `deprecated_since`, `replacement` и `removal_version`; deprecation-период не меньше двух minor-релизов; описание и колонка `deprecated` не противоречат друг другу. В PR 2a работает report-only; hard `exit 1` включается в PR 3 / финальном DoD. +- `scripts/check_spec_inventory_sync.py --output `: реализовать; пишет `spec-inventory-report.json`. Проверяет: каждая операция из `docs/avito/api/*.json` присутствует в inventory; в inventory нет операций, отсутствующих в spec; совпадают `документ + метод + путь + раздел`. В сверке «документ + метод + путь + раздел» явно задокументировать имена колонок inventory, по которым берётся поле «раздел»; если имя расходится, переименование делается в этом же PR. В PR 2a работает report-only и публикуется как CI artifact. **Область охвата**: операция-уровень (method + path). Поле-уровневая сверка типов/nullability/enum-значений SDK-моделей с OpenAPI-схемами — отдельная SDK-задача вне scope docs-плана; §14.2 scorecard закрывается через ручной DA-аудит выборки из 20 моделей при оценке. - `scripts/check_readme_domain_coverage.py`: реализовать; домены из inventory. -- `scripts/check_reference_public_surface.py --output `: реализовать; пишет `reference-public-report.json`. Проверяет: все экспорты из `avito.__all__`, `avito..__all__`, `avito.testing.__all__` попадают в reference; лишние internal/private символы не попадают в generated nav/discovery pages. В PR 2 работает report-only и публикуется как CI artifact. -- Docstring readiness audit: сформировать report-only список публичных классов/методов, где docstring не покрывает требования STYLEGUIDE. Реализация — `scripts/check_public_docstrings.py --output ` с проверкой обязательных contract-aspects, а не только наличия docstring. Не блокирует PR 2, но блокирует перевод `interrogate` в gate. +- `Makefile` — добавить в `docs-strict`: `poetry run python scripts/check_readme_domain_coverage.py`. +- CI: `inventory-coverage-report.json` и `spec-inventory-report.json` публикуются как артефакты. +- `poetry.lock`: обновить при добавлении зависимостей. + +**Критерий готовности PR 2a**: `scripts/parse_inventory.py` возвращает `list[InventoryRow]` с новыми колонками; колонки `deprecated_since`, `replacement`, `removal_version` заполнены; имя колонки `раздел` в `inventory.md` зафиксировано в `parse_inventory.py` как `section` (или эквивалент) и совпадает с тем, что читает `check_spec_inventory_sync.py`; `inventory-coverage-report.json` и `spec-inventory-report.json` публикуются как CI-артефакты (непустые gaps допустимы как report-only); `make docs-strict` проходит с `check_readme_domain_coverage.py`. + +### PR 2.5 — Runtime deprecation contract + +**Задача**: синхронизировать runtime-поведение SDK с deprecated-данными inventory. Это публичное SDK-изменение, поэтому оно отделено от генерации сайта. + +Конкретные изменения: + +- Добавить runtime `DeprecationWarning` для каждого публичного SDK-символа с `deprecated: да`, при первом вызове, с replacement и целевой версией удаления. +- Добавить/обновить docstring line у deprecated-символов: replacement и target removal version. +- Добавить `tests/contracts/test_deprecation_warnings.py`, который строит cases из inventory, но проверяет поведение через реальные публичные вызовы/минимальные fake-transport сценарии, а не только наличие атрибута. +- Добавить запись в `CHANGELOG.md` в секцию `Deprecated`; проверить, что CHANGELOG релиза содержит стандартные секции `Added`/`Changed`/`Deprecated`/`Removed`/`Fixed` (пустые секции допустимы только если политика changelog это явно разрешает). +- `scripts/check_changelog_sections.py --output changelog-sections-report.json`: проверяет, что CHANGELOG релиза содержит секции `Added`/`Changed`/`Deprecated`/`Removed`/`Fixed`; в PR 2.5 — report-only, в финальном DoD — strict. Закрывает scorecard §18.4. + +**Критерий готовности PR 2.5**: `pytest tests/contracts/test_deprecation_warnings.py` зелёный; runtime warnings не дублируются сверх первого вызова; `make test typecheck lint` зелёные; `changelog-sections-report.json` публикуется как CI artifact. + +### PR 2b — Reference: генерация, страницы, baselines + +**Prerequisite**: PR 2a и PR 2.5 слиты. PR 2a даёт `parse_inventory.py`; PR 2.5 обеспечивает runtime `DeprecationWarning`, без которого deprecated-admonition в reference преждевременен. + +Конкретные изменения: + - `pyproject.toml`, группа `docs` — добавить и обновить `poetry.lock`: ```toml mkdocstrings = { version = ">=0.27", extras = ["python"] } @@ -279,7 +346,14 @@ handlers: ``` - `mkdocs.yml`: подключить `gen-files`, `literate-nav`, `mkdocstrings[python]`. - `docs/site/assets/_gen_reference.py`: реализовать (виртуальные файлы). -- `docs/site/reference/`: создать `config.md`, `models.md`, `exceptions.md`, `pagination.md`, `testing.md`; `operations.md` генерируется виртуально из inventory. +- `docs/site/reference/`: создать следующие страницы: + - `config.md` — документирует `AvitoSettings` и `AuthSettings`, включая `user_agent_suffix` (поле для суффикса User-Agent, STYLEGUIDE § User-Agent), env-переменные и priority-resolution (`env > .env > defaults`). + - `models.md`, `pagination.md`, `testing.md`; `operations.md` генерируется виртуально из inventory. + - `enums.md` — генерируется `_gen_reference.py` (виртуальный файл). + - `client.md` — полный reference включая `debug_info()` с документированным security-контрактом. + - `exceptions.md` — документирует поля `operation`, `status`, `request_id`, `attempt`, `method`, `endpoint` для каждого публичного исключения. +- `scripts/check_reference_public_surface.py --output `: реализовать; пишет `reference-public-report.json`. Проверяет: все экспорты из `avito.__all__`, `avito..__all__`, `avito.testing.__all__`, `avito.AvitoClient.debug_info` и все `Enum`-классы из `avito..__all__` (через `reference/enums.md`) попадают в reference; лишние internal/private символы не попадают в generated nav/discovery pages. В PR 2b работает report-only и публикуется как CI artifact. +- Docstring readiness audit: `scripts/check_public_docstrings.py --output ` проверяет шесть обязательных contract-аспектов каждого публичного метода: (1) возвращаемая SDK-модель, (2) nullable/empty behavior, (3) каждый поддерживаемый per-operation override из канонического списка `reference/config.md`, (4) идемпотентность, (5) типовые исключения (Raises) с перечислением метаданных, (6) поведение при `dry_run=True` для write-методов. В PR 2b report-only; строгий gate — в PR 3 / финальном DoD. Не блокирует PR 2b, но блокирует перевод `interrogate` в gate. - `Makefile`: ```makefile docs-strict: @@ -294,34 +368,27 @@ handlers: {"modules": {"avito/accounts/client.py": 92.5, ...}, "generated_at": "", "interrogate_version": ""} ``` -**Критерий готовности PR 2**: все пункты STYLEGUIDE § What Constitutes the Public SDK Contract имеют reference-страницу или явно отмеченный docstring gap; deprecated-бейджи рендерятся; `reference/operations.md` строится из inventory; `make docs-strict` проходит; `inventory-coverage-report.json`, `spec-inventory-report.json`, `reference-public-report.json` и `docstring-contract-report.json` публикуются как CI-артефакты; колонки `deprecated_since`/`replacement`/`removal_version` заполнены для всех `deprecated: да` записей. Непустые SDK coverage/spec-sync/reference-public/docstring gaps допустимы только как report-only артефакты PR 2 и должны быть закрыты к финальному DoD. - -### PR 2.5 — Runtime deprecation contract - -**Задача**: синхронизировать runtime-поведение SDK с deprecated-данными inventory. Это публичное SDK-изменение, поэтому оно отделено от генерации сайта. - -Конкретные изменения: - -- Добавить runtime `DeprecationWarning` для каждого публичного SDK-символа с `deprecated: да`, при первом вызове, с replacement и целевой версией удаления. -- Добавить/обновить docstring line у deprecated-символов: replacement и target removal version. -- Добавить `tests/contracts/test_deprecation_warnings.py`, который строит cases из inventory, но проверяет поведение через реальные публичные вызовы/минимальные fake-transport сценарии, а не только наличие атрибута. -- Добавить запись в `CHANGELOG.md` в секцию `Deprecated`; проверить, что CHANGELOG релиза содержит стандартные секции `Added`/`Changed`/`Deprecated`/`Removed`/`Fixed` (пустые секции допустимы только если политика changelog это явно разрешает). - -**Критерий готовности PR 2.5**: `pytest tests/contracts/test_deprecation_warnings.py` зелёный; runtime warnings не дублируются сверх первого вызова; `make test typecheck lint` зелёные. +**Критерий готовности PR 2b**: все семь пунктов STYLEGUIDE § What Constitutes the Public SDK Contract имеют reference-страницу с явным файловым маппингом в `docs-quality-report.json.public_contract_coverage`: `AvitoClient` → `client.md`, `AvitoSettings/AuthSettings` → `config.md`, resource factory methods → `operations.md` + `domains/*.md`, public models → `models.md` + `domains/*.md`, typed exceptions → `exceptions.md`, `PaginatedList` → `pagination.md`, `to_dict()`/`model_dump()` → `models.md`, `debug_info()` → `client.md`; `reference/config.md` документирует `user_agent_suffix` и priority-resolution; deprecated-бейджи рендерятся; `reference/operations.md` строится из inventory; `make docs-strict` проходит; `inventory-coverage-report.json`, `spec-inventory-report.json`, `reference-public-report.json` и `docstring-contract-report.json` публикуются как CI-артефакты; колонки `deprecated_since`/`replacement`/`removal_version` заполнены для всех `deprecated: да` записей. Непустые SDK coverage/spec-sync/reference-public/docstring gaps допустимы только как report-only артефакты PR 2b и должны быть закрыты к финальному DoD. ### PR 3 — How-to, explanations и quality gates Конкретные изменения: -- `docs/site/how-to/*` — 14 рецептов с фиксированными файлами: +- `docs/site/how-to/*` — 17 рецептов с фиксированными файлами и явным доменным покрытием: `auth-and-config.md`, `chat-image-upload.md`, `promotion-dry-run.md`, `pagination.md`, `order-labels.md`, `job-applications.md`, `autoteka-report.md`, `realty-booking.md`, `cpa-calltracking.md`, `ratings-and-tariffs.md`, `per-operation-overrides.md`, - `idempotency.md`, `testing-with-fake-transport.md`, `diagnostics-and-logging.md`. -- `docs/site/explanations/*` — 8 концептуальных статей с Mermaid: + `idempotency.md`, `testing-with-fake-transport.md`, `diagnostics-and-logging.md`, + `security-practices.md` (сквозной документ: редакция секретов для consumer-кода, дополняет `explanations/security-and-redaction.md`), + `account-profile.md` (домен `accounts`: `get_self()`, баланс, иерархия), + `ad-listing-and-stats.md` (домен `ads`: листинг, статистика, пагинация объявлений). +- `docs/site/explanations/*` — концептуальные статьи с Mermaid: `architecture.md`, `auth-flow.md`, `transport-and-retries.md`, `error-model.md`, `pagination-semantics.md`, `dry-run-and-idempotency.md`, `testing-strategy.md`, - `api-coverage-and-deprecations.md`. + `api-coverage-and-deprecations.md`, + `config-resolution.md` (priority-resolution `env > .env > defaults`, детерминированность, scorecard §13.2), + `security-and-redaction.md` — security-модель и редакция секретов (покрывает scorecard §7.1–§7.4 через DA). +- `docs/site/tutorials/first-promotion.md`: написать содержимое (сейчас placeholder); tutorial `dry_run=True` → `dry_run=False` с mktestdocs harness. - До массового написания how-to: обновить `README.md` и уже существующие tutorial-snippet'ы под реальные публичные сигнатуры текущего SDK. Устаревшие примеры с `request=` DTO там, где сигнатура уже flattened, переписываются, а не помечаются как “illustrative”. - Перед `testing-with-fake-transport.md`: добавить публичный consumer-testing API. Выбранный контракт: `FakeTransport.as_client(*, user_id: int | None = None, retry_policy: RetryPolicy | None = None) -> AvitoClient`. Он создаёт полностью инициализированный `AvitoClient` поверх fake transport без post-init monkeypatch приватных полей и без публичного параметра `transport` в `AvitoClient.__init__`. `FakeTransport.build()` не используется в пользовательской документации; если он остаётся, он помечается как low-level/internal testing helper или проходит deprecation policy. - Перед массовым написанием рецептов: реализовать mktestdocs harness на `getting-started.md` и одном how-to, прогнать `pytest tests/docs/`, затем масштабировать на остальные страницы. @@ -340,13 +407,73 @@ handlers: - `pydocstyle` с профилем Google — `make qa-docs`, **не** `make lint`. - `interrogate` gate — diff против `origin/main`; per-module vs baseline. `ci.yml`: `fetch-depth: 0`. - `CONTRIBUTING.md`: инструкция по установке lychee (`brew install lychee` / `cargo binstall lychee`); review-чек-лист README domain coverage. -- `.github/pull_request_template.md`: чек-лист coverage. +- `.github/pull_request_template.md`: чек-лист coverage; добавить чек-бокс «Публичное переименование: alias сохранён + `DeprecationWarning` + запись в CHANGELOG Deprecated» для supporting-gate 18.5. - `.github/workflows/docs.yml`: шаг `lycheeverse/lychee-action` (не вызов `make docs-check`); на `push` в `main` выполняются `mike deploy --push --update-aliases main latest` и `mike set-default --push latest`; на `push` тега `v*` выполняется `mike deploy --push --update-aliases stable`, где `` берётся из тега без `v`. -- `.github/workflows/ci.yml`: добавить `make docs-strict` в пайплайн; `fetch-depth: 0`. +- `.github/workflows/ci.yml`: добавить `make docs-strict` в пайплайн; `fetch-depth: 0`; добавить `bandit -r avito/` как supporting-gate для 7.5 (report-only на этом этапе, strict — отдельная SDK-задача вне scope docs-плана). - `Makefile`: в PR 3 расширить `docs-strict`, добавив `poetry run pytest tests/docs/`. В PR 2 snippet `docs-strict` ещё не включает mktestdocs. -- `docs-quality-report.md` или CI artifact `docs-quality-report.json`: фиксирует Diátaxis-матрицу, прохождение 15.1–15.6, TTFC-замер, README/domain coverage, markdown examples, lychee, inventory coverage, spec↔inventory sync, reference public surface, docstring readiness, а также supporting-gates для scorecard §16 и §18. +- CI artifact `docs-quality-report.json`: воспроизводимый отчёт с фиксированной схемой (поля ниже обязательны; `null` = не выполнено). Без строгой схемы отчёт нельзя сравнивать между релизами и он не закрывает scorecard §28.1 (±5% между оценщиками): + ```json + { + "generated_at": "", + "sdk_version": "", + "diataxis_matrix": { + "tutorials": ["getting-started.md", "first-promotion.md"], + "how-to": ["auth-and-config.md", "...17 files total..."], + "reference": ["client.md", "config.md", "models.md", "..."], + "explanations": ["architecture.md", "...10 files total..."] + }, + "domain_howto_coverage": { + "accounts": "account-profile.md", + "ads": "ad-listing-and-stats.md", + "autoteka": "autoteka-report.md", + "...": "..." + }, + "public_contract_coverage": { + "AvitoClient": "client.md", + "AvitoSettings": "config.md", + "AuthSettings": "config.md", + "factory_methods": "operations.md", + "public_models": "models.md", + "typed_exceptions": "exceptions.md", + "PaginatedList": "pagination.md", + "serialization": "models.md", + "debug_info": "client.md" + }, + "disabled_criteria": ["12"], + "subcriteria": { + "15.1": {"grade": 1.0, "evidence": "getting-started.md проходит TT-процедуру"}, + "15.2": {"grade": 1.0, "evidence": "17 рецептов, все домены покрыты"}, + "15.3": {"grade": null, "evidence": "interrogate baseline + reference pages"}, + "15.4": {"grade": null, "evidence": "10 explanations"}, + "15.5": {"grade": null, "evidence": "CHANGELOG.md обновлён"}, + "15.6": {"grade": null, "evidence": "pytest tests/docs/ зелёный"} + }, + "supporting_gates": { + "7.3_debug_info_safe_by_default": null, + "7.5_bandit_high_severity": null, + "16.1_fake_transport_namespace": null, + "16.2_mock_contract_documented": null, + "16.3_json_serializable_models": null, + "16.4_context_manager_close": null, + "18.1_semver_compliant": null, + "18.2_deprecation_period_2minor": null, + "18.3_deprecation_warning_emitted": null, + "18.4_changelog_sections": null, + "18.5_public_renames_via_alias": null + }, + "ttfc_minutes": null, + "lychee_broken_links": 0, + "placeholder_count": 0, + "inventory_coverage_gaps": 0, + "spec_inventory_gaps": 0, + "reference_public_gaps": 0, + "docstring_contract_gaps": 0, + "reference_explanation_examples_gaps": 0, + "changelog_sections_gaps": 0 + } + ``` -**Критерий готовности PR 3**: Diátaxis-матрица 4×N; каждый публичный домен (из inventory, кроме auth/core/testing) имеет ≥1 how-to; README/tutorials/how-to синхронизированы с реальными публичными сигнатурами SDK; все `python`/`pycon` блоки в README/tutorials/how-to исполняются через mktestdocs с harness conftest; docs-harness surface проверен отдельным тестом; SDK-примеры в reference/explanations либо исполняются, либо не помечены как executable; `make docs-strict`, `make qa-docs` и CI lychee-step проходят; `mike list` показывает как минимум `main [latest]` и текущий релиз `[stable]`; scorecard §15.1–15.6 закрыт по каждому подпункту, а `docs-quality-report` показывает supporting-gates для §16 и §18. +**Критерий готовности PR 3**: Diátaxis-матрица 4×N; каждый публичный домен (из inventory, кроме auth/core/testing) имеет ≥1 how-to с явным файловым маппингом в `domain_howto_coverage` поле `docs-quality-report.json` (включая `accounts` → `account-profile.md` и `ads` → `ad-listing-and-stats.md`); README/tutorials/how-to синхронизированы с реальными публичными сигнатурами SDK; все `python`/`pycon` блоки в README/tutorials/how-to исполняются через mktestdocs с harness conftest; docs-harness surface проверен отдельным тестом; SDK-примеры в reference/explanations либо исполняются, либо не помечены как executable; `make docs-strict`, `make qa-docs` и CI lychee-step проходят; `mike list` показывает как минимум `main [latest]` и текущий релиз `[stable]`; scorecard §15.1–15.6 закрыт по каждому подпункту; все `supporting_gates.*` заполнены `grade` и `evidence` (`null` запрещён); `public_contract_coverage` заполнено всеми девятью ключами; `disabled_criteria` содержит `["12"]` с пояснением в release notes. ## Риски и их нейтрализация @@ -372,11 +499,23 @@ handlers: | В production docs остаются плейсхолдеры | `tests/docs/test_no_placeholders.py` и финальный `rg`-gate на `Раздел в разработке|placeholder|плейсхолдер|TODO|TBD|coming soon` | | README/snippet'ы отстают от реальных public signatures | Обязательная синхронизация примеров в PR 3 + mktestdocs + review-чек-лист | | Docs-harness начинает жить отдельно от реального API | `tests/docs/test_docs_harness_surface.py` сверяет facade с `AvitoClient` и доменными public methods | +| Harness не покрывает новый endpoint из how-to: `FakeTransport._handle` падает без понятного сообщения | Добавить в harness-fallback сообщение «маршрут не прописан в conftest, добавь route_sequence для »; каждый новый how-to Python-блок проверяется в `pytest tests/docs/` до merge | +| Поле-уровневая сверка типов/nullability/enum SDK-моделей с OpenAPI-схемами отсутствует в скриптах | Явно исключить из scope: `check_spec_inventory_sync.py` покрывает только operation-уровень; §14.2 scorecard закрывается ручным DA-аудитом выборки 20 моделей при финальной оценке; полноценный `check_spec_model_sync.py` — отдельная SDK-задача | +| `debug_info()` не попадает в reference — STYLEGUIDE-нарушение | `_gen_reference.py` hard error при отсутствии символа; `check_reference_public_surface.py` strict в финальном DoD | +| Enum-контракт теряется: новый enum в `avito..enums.py` не попал в `__all__` | `check_reference_public_surface.py` сверяет `reference/enums.md` со всеми `Enum` подклассами во всех публичных доменных пакетах | +| Exception metadata не документированы — scorecard §6.3 провал | `check_public_docstrings.py` шестой аспект (Raises + metadata fields); `reference/exceptions.md` strict gate | +| Per-operation overrides описаны неконсистентно между методами | Канонический набор в `reference/config.md`; `check_public_docstrings.py` сверяет | +| Mermaid не рендерится в strict-mode | Смоук-тест mermaid в PR 1 на `explanations/architecture.md` placeholder | +| Scorecard §12 искажает финальный Score, т.к. SDK sync | `disabled_criteria: ["12"]` + перераспределение весов зафиксировано в отчёте | +| `supporting_gates.*` остаются `null` и DoD пройден формально | DoD запрещает `null`; PR 3 критерий готовности требует `grade` + `evidence` для каждого | +| PR 2b мержится раньше PR 2.5 — admonition без runtime warning | PR 2.5 prerequisite для PR 2b; зафиксировано в «Зафиксированных решениях» | ## Реиспользуемые артефакты - `avito/testing/__init__.py` (после PR 1: `FakeTransport`, `FakeResponse`, `JsonValue`, `RecordedRequest`, `json_response`, `route_sequence`; после PR 3: `FakeTransport.as_client()`) — harness conftest, how-to, reference. -- `avito/core/exceptions.py` — reference `exceptions.md`, explanation `error-model.md`. +- `avito/core/exceptions.py` поля `operation`, `status`, `request_id`, `attempt`, `method`, `endpoint` — документируются в reference `exceptions.md` и explanation `error-model.md`. +- `avito//enums.py` (все домены) — `reference/enums.md` (генерируется). +- `AvitoClient.debug_info` — `reference/client.md`, `explanations/security-and-redaction.md`. - `avito/core/pagination.py:PaginatedList` — reference `pagination.md`, explanation `pagination-semantics.md`. - `avito/core/serialization.py:SerializableModel` — reference `models.md`. - `docs/avito/inventory.md` — парсится через `parse_inventory.py`; источник доменов, deprecated-статусов, `deprecated_since`, `replacement`, `removal_version`. @@ -385,7 +524,7 @@ handlers: ## Definition of Done (итоговая) -- Все PR 1, PR 2, PR 2.5 и PR 3 смержены. +- Все PR 1, PR 2a, PR 2.5, PR 2b и PR 3 смержены. - `mkdocs build --strict` — без предупреждений. - CI lychee-step — ноль битых ссылок. - Все `python`/`pycon` блоки в README/tutorials/how-to исполняются через mktestdocs с harness conftest в `pytest tests/docs/`. @@ -397,11 +536,23 @@ handlers: - `spec-inventory-report.json` пуст. - `reference-public-report.json` пуст. - `docs-quality-report` опубликован как CI artifact и показывает 15.1–15.6 без пропусков, а также supporting-gates для scorecard §16 и §18. +- Все supporting-gates в `docs-quality-report.json` имеют `grade` и `evidence` (запрет `null`). +- `public_contract_coverage` заполнено по всем девяти пунктам STYLEGUIDE § What Constitutes the Public SDK Contract. +- `disabled_criteria` содержит `["12"]` с обоснованием. +- `reference/enums.md` сгенерирован и покрывает все публичные `Enum` из `avito..__all__`. +- `reference/exceptions.md` документирует все поля exception metadata (`operation`, `status`, `request_id`, `attempt`, `method`, `endpoint`) для каждого публичного исключения. +- `reference/client.md` документирует `debug_info()` с security-контрактом. +- `explanations/security-and-redaction.md` опубликован. +- `changelog-sections-report.json` пуст (CHANGELOG релиза содержит все пять секций). +- `reference-explanation-examples-report.json` пуст. +- TTFC измерен вручную перед финальным DoD и зафиксирован в `ttfc_minutes`. +- `bandit -r avito/` отчёт опубликован как CI artifact (supporting-gate 7.5). +- Review-чек-лист в `.github/pull_request_template.md` содержит пункт про alias-переименование. - Docstring readiness gaps закрыты для публичных контрактов, попадающих в generated reference. -- Diátaxis-матрица 4×N; каждый публичный домен (кроме auth/core/testing) имеет ≥1 how-to; `make docs-strict` проходит полностью. +- Diátaxis-матрица 4×N; каждый публичный домен (кроме auth/core/testing) имеет ≥1 how-to с явным маппингом в `docs-quality-report.json`; `make docs-strict` проходит полностью. - Reference `operations.md` даёт карту всех inventory operations к публичным SDK-методам. - Reference `testing.md` и how-to `testing-with-fake-transport.md` покрывают все аспекты public testing contract: scripting responses, call inspection, transport-level errors, `Retry-After`, `as_client()` consumer test. -- Каждый пункт STYLEGUIDE § What Constitutes the Public SDK Contract покрыт reference-страницей. +- Все семь пунктов STYLEGUIDE § What Constitutes the Public SDK Contract имеют reference-страницу, явный файловый маппинг зафиксирован в `docs-quality-report.json.public_contract_coverage`. - README/tutorials/how-to snippet'ы соответствуют актуальным публичным сигнатурам SDK; устаревших примеров с pre-refactor `request=` DTO в flattened-methods не осталось. - В production docs нет плейсхолдеров: `Раздел в разработке`, `placeholder`, `плейсхолдер`, `TODO`, `TBD`, `coming soon`. - `mike list` показывает `main [latest]` и как минимум один релизный docs-version с alias `stable`; root redirect ведёт на `latest`. @@ -414,12 +565,16 @@ handlers: 4. `make docs-check` — дополнительно lychee (требует `brew install lychee`). 5. `make qa-docs` — `pydocstyle` с профилем Google. 6. `poetry run pytest tests/docs/` — исполняет README/tutorials/how-to snippets и проверяет отсутствие плейсхолдеров. -7. TTFC-процедура: чистый venv, `pip install avito-py`, tutorial, засечь время до реального `get_self()` с настоящими ключами. +7. TTFC-процедура (runbook в `CONTRIBUTING.md`): чистый venv, `pip install avito-py`, tutorial, секундомер до реального `get_self()` с настоящими ключами; результат пишется в `ttfc_minutes` текущего `docs-quality-report.json`. Выполняется ответственным мейнтейнером перед финальным DoD. 8. `pytest tests/contracts/test_deprecation_warnings.py` — для каждого SDK-символа с `deprecated: да`. 9. `python scripts/check_inventory_coverage.py --strict --output inventory-coverage-report.json` — exit 0. 10. `python scripts/check_spec_inventory_sync.py --strict --output spec-inventory-report.json` — exit 0. 11. `python scripts/check_reference_public_surface.py --strict --output reference-public-report.json` — exit 0. 12. `python scripts/check_public_docstrings.py --strict --output docstring-contract-report.json` — exit 0 после закрытия gaps. -13. `rg -n "Раздел в разработке|placeholder|плейсхолдер|TODO|TBD|coming soon" docs/site README.md` — пустой вывод для production docs. -14. CI: PR с битой ссылкой → lychee-step падает; PR с пониженным coverage → interrogate падает. -15. Push в `main` → `mike deploy --push --update-aliases main latest` + `mike set-default --push latest`; push тега `v*` → `mike deploy --push --update-aliases stable`; `mike list` показывает оба alias. +13. `python scripts/check_changelog_sections.py --strict --output changelog-sections-report.json` — exit 0. +14. `python scripts/check_docs_examples.py --strict --output reference-explanation-examples-report.json` — exit 0. +15. `bandit -r avito/` — supporting-gate 7.5, отчёт без high-severity в публичных модулях. +16. `mkdocs build --strict` рендерит mermaid в `architecture.md` без предупреждений. +17. `rg -n "Раздел в разработке|placeholder|плейсхолдер|TODO|TBD|coming soon" docs/site README.md` — пустой вывод для production docs. +18. CI: PR с битой ссылкой → lychee-step падает; PR с пониженным coverage → interrogate падает. +19. Push в `main` → `mike deploy --push --update-aliases main latest` + `mike set-default --push latest`; push тега `v*` → `mike deploy --push --update-aliases stable`; `mike list` показывает оба alias. From 58312c51a60ccd88576a687cf636e649b9ae0b5d Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Fri, 24 Apr 2026 00:43:10 +0300 Subject: [PATCH 3/9] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 26 +- .gitignore | 7 +- CHANGELOG.md | 4 + Makefile | 15 +- README.md | 2 +- avito/ads/domain.py | 45 +- avito/core/deprecation.py | 55 ++ avito/cpa/domain.py | 40 + avito/testing/__init__.py | 18 +- docs/avito/inventory.md | 412 +++++----- docs/site/.pages | 7 + docs/site/assets/_gen_reference.py | 135 ++++ docs/site/explanations/.pages | 1 + docs/site/explanations/architecture.md | 18 + docs/site/explanations/index.md | 2 +- docs/site/how-to/.pages | 1 + docs/site/how-to/auth-and-config.md | 8 + docs/site/index.md | 4 +- docs/site/reference/.pages | 10 + docs/site/reference/client.md | 22 + docs/site/reference/config.md | 43 ++ docs/site/reference/coverage.md | 34 + docs/site/reference/exceptions.md | 25 + docs/site/reference/index.md | 29 +- docs/site/reference/models.md | 26 + docs/site/reference/pagination.md | 7 + docs/site/reference/testing.md | 18 + mkdocs.yml | 31 +- poetry.lock | 138 +++- pyproject.toml | 7 +- scripts/build_docs_quality_report.py | 193 +++++ scripts/check_inventory_coverage.py | 120 +++ scripts/check_public_docstrings.py | 110 +++ scripts/check_readme_domain_coverage.py | 62 ++ scripts/check_reference_public_surface.py | 140 ++++ scripts/check_spec_inventory_sync.py | 131 ++++ scripts/parse_inventory.py | 182 +++++ tests/contracts/test_deprecation_warnings.py | 98 +++ tests/domains/cpa/test_cpa.py | 17 +- usability_scorecard.md | 742 +++++++++---------- 40 files changed, 2363 insertions(+), 622 deletions(-) create mode 100644 avito/core/deprecation.py create mode 100644 docs/site/.pages create mode 100644 docs/site/assets/_gen_reference.py create mode 100644 docs/site/explanations/architecture.md create mode 100644 docs/site/how-to/auth-and-config.md create mode 100644 docs/site/reference/client.md create mode 100644 docs/site/reference/config.md create mode 100644 docs/site/reference/coverage.md create mode 100644 docs/site/reference/exceptions.md create mode 100644 docs/site/reference/models.md create mode 100644 docs/site/reference/pagination.md create mode 100644 docs/site/reference/testing.md create mode 100644 scripts/build_docs_quality_report.py create mode 100644 scripts/check_inventory_coverage.py create mode 100644 scripts/check_public_docstrings.py create mode 100644 scripts/check_readme_domain_coverage.py create mode 100644 scripts/check_reference_public_surface.py create mode 100644 scripts/check_spec_inventory_sync.py create mode 100644 scripts/parse_inventory.py create mode 100644 tests/contracts/test_deprecation_warnings.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a66f9c7..254cc72 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,7 +35,31 @@ jobs: run: poetry install --no-interaction --with docs - name: Build docs (strict) - run: poetry run mkdocs build --strict + run: make docs-strict + + - name: Build docs reports + run: | + poetry run python scripts/check_inventory_coverage.py --output inventory-coverage-report.json + poetry run python scripts/check_spec_inventory_sync.py --output spec-inventory-report.json + poetry run python scripts/check_reference_public_surface.py --output reference-public-report.json + poetry run python scripts/check_public_docstrings.py --output docstring-contract-report.json + poetry run python scripts/build_docs_quality_report.py \ + --inventory-report inventory-coverage-report.json \ + --spec-report spec-inventory-report.json \ + --reference-report reference-public-report.json \ + --docstring-report docstring-contract-report.json \ + --output docs-quality-report.json + + - name: Upload docs reports + uses: actions/upload-artifact@v4 + with: + name: docs-contract-reports + path: | + inventory-coverage-report.json + spec-inventory-report.json + reference-public-report.json + docstring-contract-report.json + docs-quality-report.json deploy: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 93470ad..66ed71a 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,11 @@ venv.bak/ # mkdocs documentation /site +inventory-coverage-report.json +spec-inventory-report.json +reference-public-report.json +docstring-contract-report.json +docs-quality-report.json # mypy .mypy_cache/ @@ -140,4 +145,4 @@ dmypy.json .pytype/ # Cython debug symbols -cython_debug/ \ No newline at end of file +cython_debug/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1269823..8eb98a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to Semantic Versioning. ## [Unreleased] +### Deprecated +- Архивные CPA-методы `CpaArchive.get_call`, `CpaArchive.get_balance_info`, `CpaArchive.get_call_by_id` и режим `CpaChat.list(version=1)` теперь эмитируют `DeprecationWarning` при первом вызове; используйте `call_tracking_call().download`, `cpa_lead().get_balance_info`, `call_tracking_call().get` и `cpa_chat().list(version=2)`. +- Архивные методы автозагрузки `AutoloadArchive.get_profile`, `AutoloadArchive.save_profile`, `AutoloadArchive.get_last_completed_report`, `AutoloadArchive.get_report` теперь эмитируют `DeprecationWarning` при первом вызове; используйте `autoload_profile().get`, `autoload_profile().save`, `autoload_report().get_last_completed` и `autoload_report().get`. + ### Changed - Централизовано выполнение схемы `request + map` через `Transport.request_public_model`. - Убраны прямые обращения доменных клиентов к `request_json` и приватному `Transport._auth_provider`. diff --git a/Makefile b/Makefile index 4ffebe7..9b95ea2 100644 --- a/Makefile +++ b/Makefile @@ -44,5 +44,18 @@ release: docs-serve: poetry run mkdocs serve -docs-build: +docs-strict: poetry run mkdocs build --strict + poetry run python scripts/check_readme_domain_coverage.py + +docs-build: docs-strict + +docs-report: + poetry run python scripts/check_inventory_coverage.py --output inventory-coverage-report.json + poetry run python scripts/check_spec_inventory_sync.py --output spec-inventory-report.json + poetry run python scripts/check_reference_public_surface.py --output reference-public-report.json + poetry run python scripts/check_public_docstrings.py --output docstring-contract-report.json + poetry run python scripts/build_docs_quality_report.py + +docs-check: docs-strict + lychee --exclude "avito\.ru" --retry-wait-time 5 --max-retries 3 --timeout 30 site/ diff --git a/README.md b/README.md index 6d310dd..5178886 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SDK для Avito -[![CI](https://github.com/p141592/avito/actions/workflows/ci.yml/badge.svg)](https://github.com/p141592/avito/actions/workflows/ci.yml) +[![CI](https://github.com/p141592/avito_python_api/actions/workflows/ci.yml/badge.svg)](https://github.com/p141592/avito_python_api/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/p141592/avito_python_api/badge.svg?branch=main)](https://coveralls.io/github/p141592/avito_python_api?branch=main) [![PyPI Downloads](https://img.shields.io/pypi/dm/avito-py.svg)](https://pypi.org/project/avito-py/) [![API coverage](https://img.shields.io/badge/API%20coverage-204%2F204-success)](docs/avito/inventory.md) diff --git a/avito/ads/domain.py b/avito/ads/domain.py index f411a98..188e8fd 100644 --- a/avito/ads/domain.py +++ b/avito/ads/domain.py @@ -37,6 +37,7 @@ VasPricesResult, ) from avito.core import PaginatedList, ValidationError +from avito.core.deprecation import deprecated_method from avito.core.domain import DomainObject from avito.core.validation import ( validate_non_empty_string, @@ -438,11 +439,26 @@ class AutoloadArchive(DomainObject): report_id: int | str | None = None + @deprecated_method( + symbol="AutoloadArchive.get_profile", + replacement="autoload_profile().get", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) def get_profile(self) -> AutoloadProfileSettings: - """Получает архивный профиль автозагрузки.""" + """Получает архивный профиль автозагрузки. + + Deprecated: используйте `autoload_profile().get`; удаление в версии 1.3.0. + """ return AutoloadArchiveClient(self.transport).get_profile() + @deprecated_method( + symbol="AutoloadArchive.save_profile", + replacement="autoload_profile().save", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) def save_profile( self, *, @@ -451,7 +467,10 @@ def save_profile( callback_url: str | None = None, idempotency_key: str | None = None, ) -> AdsActionResult: - """Сохраняет архивный профиль автозагрузки.""" + """Сохраняет архивный профиль автозагрузки. + + Deprecated: используйте `autoload_profile().save`; удаление в версии 1.3.0. + """ return AutoloadArchiveClient(self.transport).save_profile( is_enabled=is_enabled, @@ -460,13 +479,31 @@ def save_profile( idempotency_key=idempotency_key, ) + @deprecated_method( + symbol="AutoloadArchive.get_last_completed_report", + replacement="autoload_report().get_last_completed", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) def get_last_completed_report(self) -> LegacyAutoloadReport: - """Получает архивную статистику по последней выгрузке.""" + """Получает архивную статистику по последней выгрузке. + + Deprecated: используйте `autoload_report().get_last_completed`; удаление в версии 1.3.0. + """ return AutoloadArchiveClient(self.transport).get_last_completed_report() + @deprecated_method( + symbol="AutoloadArchive.get_report", + replacement="autoload_report().get", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) def get_report(self) -> LegacyAutoloadReport: - """Получает архивную статистику по конкретной выгрузке.""" + """Получает архивную статистику по конкретной выгрузке. + + Deprecated: используйте `autoload_report().get`; удаление в версии 1.3.0. + """ report_id = self._require_report_id() return AutoloadArchiveClient(self.transport).get_report(report_id=report_id) diff --git a/avito/core/deprecation.py b/avito/core/deprecation.py new file mode 100644 index 0000000..7033015 --- /dev/null +++ b/avito/core/deprecation.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import warnings +from collections.abc import Callable +from functools import wraps +from typing import ParamSpec, TypeVar + +P = ParamSpec("P") +R = TypeVar("R") + +_WARNED_SYMBOLS: set[str] = set() + + +def warn_deprecated_once( + *, + symbol: str, + replacement: str, + removal_version: str, + deprecated_since: str, +) -> None: + if symbol in _WARNED_SYMBOLS: + return + _WARNED_SYMBOLS.add(symbol) + warnings.warn( + ( + f"`{symbol}` устарел с версии {deprecated_since}; " + f"используйте `{replacement}`. " + f"Удаление запланировано в версии {removal_version}." + ), + DeprecationWarning, + stacklevel=3, + ) + + +def deprecated_method( + *, + symbol: str, + replacement: str, + removal_version: str, + deprecated_since: str, +) -> Callable[[Callable[P, R]], Callable[P, R]]: + def decorate(method: Callable[P, R]) -> Callable[P, R]: + @wraps(method) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> R: + warn_deprecated_once( + symbol=symbol, + replacement=replacement, + removal_version=removal_version, + deprecated_since=deprecated_since, + ) + return method(*args, **kwargs) + + return wrapped + + return decorate diff --git a/avito/cpa/domain.py b/avito/cpa/domain.py index 374202e..5c69714 100644 --- a/avito/cpa/domain.py +++ b/avito/cpa/domain.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from avito.core import ValidationError +from avito.core.deprecation import deprecated_method, warn_deprecated_once from avito.core.domain import DomainObject from avito.cpa.client import ( CallTrackingClient, @@ -71,6 +72,12 @@ def list( ) -> CpaChatsResult: client = CpaChatsClient(self.transport) if version == 1: + warn_deprecated_once( + symbol="CpaChat.list(version=1)", + replacement="cpa_chat().list(version=2)", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) return client.list_by_time_classic(created_at_from=created_at_from, limit=limit) return client.list_by_time(created_at_from=created_at_from, limit=limit) @@ -110,15 +117,48 @@ class CpaArchive(DomainObject): call_id: int | str | None = None user_id: int | str | None = None + @deprecated_method( + symbol="CpaArchive.get_call", + replacement="call_tracking_call().download", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) def get_call(self, *, call_id: int | str | None = None) -> CpaAudioRecord: + """Получает архивную запись звонка. + + Deprecated: используйте `call_tracking_call().download`; удаление в версии 1.3.0. + """ + return CpaArchiveClient(self.transport).get_record( call_id=call_id or self._require_call_id() ) + @deprecated_method( + symbol="CpaArchive.get_balance_info", + replacement="cpa_lead().get_balance_info", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) def get_balance_info(self) -> CpaBalanceInfo: + """Получает архивный баланс CPA. + + Deprecated: используйте `cpa_lead().get_balance_info`; удаление в версии 1.3.0. + """ + return CpaArchiveClient(self.transport).get_balance_info() + @deprecated_method( + symbol="CpaArchive.get_call_by_id", + replacement="call_tracking_call().get", + removal_version="1.3.0", + deprecated_since="1.1.0", + ) def get_call_by_id(self, *, call_id: int) -> CpaCallInfo: + """Получает архивные данные звонка. + + Deprecated: используйте `call_tracking_call().get`; удаление в версии 1.3.0. + """ + return CpaArchiveClient(self.transport).get_call_by_id(call_id=call_id) def _require_call_id(self) -> str: diff --git a/avito/testing/__init__.py b/avito/testing/__init__.py index cdcdc14..fa06a8e 100644 --- a/avito/testing/__init__.py +++ b/avito/testing/__init__.py @@ -1,5 +1,19 @@ """Публичные тестовые утилиты SDK.""" -from avito.testing.fake_transport import FakeResponse, FakeTransport +from avito.testing.fake_transport import ( + FakeResponse, + FakeTransport, + JsonValue, + RecordedRequest, + json_response, + route_sequence, +) -__all__ = ("FakeTransport", "FakeResponse") +__all__ = ( + "FakeTransport", + "FakeResponse", + "JsonValue", + "RecordedRequest", + "json_response", + "route_sequence", +) diff --git a/docs/avito/inventory.md b/docs/avito/inventory.md index 0db947b..c4c98b3 100644 --- a/docs/avito/inventory.md +++ b/docs/avito/inventory.md @@ -37,210 +37,210 @@ ## Операции -| раздел | документ | метод | путь | описание | deprecated | пакет_sdk | доменный_объект | публичный_метод_sdk | тип_запроса | тип_ответа | тип_теста | примечания | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| promotion | CPA-аукцион.json | GET | /auction/1/bids | Получение информации о действующих и доступных ставках | нет | promotion | CpaAuction | get_user_bids | NoRequest | GetUserBidsResponse | контракт+маппинг | | -| promotion | CPA-аукцион.json | POST | /auction/1/bids | Сохранение новых ставок | нет | promotion | CpaAuction | create_item_bids | CreateItemBidsRequest | EmptyResponse | контракт+маппинг | | -| cpa | CPAАвито.json | GET | /cpa/v1/call/{call_id} | Запись звонка (deprecated) | да | cpa | CpaLegacy | legacy_get_call | NoRequest | EmptyResponse | контракт+маппинг | | -| cpa | CPAАвито.json | GET | /cpa/v1/chatByActionId/{actionId} | Чат | нет | cpa | CpaChat | get_chat_by_action_id | NoRequest | GetChatByActionIdResponse | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v1/chatsByTime | Чаты по времени (deprecated) | нет | cpa | CpaChat | create_chats_by_time | CreateChatsByTimeRequest | CreateChatsByTimeResponse | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v1/createComplaint | Создание жалобы для звонков | нет | cpa | CpaCall | create_create_complaint | CreateCreateComplaintRequest | CreateCreateComplaintResponse | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v1/createComplaintByActionId | Создание жалобы для звонков/чатов | нет | cpa | CpaLead | create_complaint_by_action_id | CreateComplaintByActionIdRequest | CreateComplaintByActionIdResponse | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v1/phonesInfoFromChats | Информация по номерам телефонов из целевых чатов | нет | cpa | CpaChat | get_phones_info_from_chats | GetPhonesInfoFromChatsRequest | GetPhonesInfoFromChatsResponse | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v2/balanceInfo | Баланс (deprecated) | да | cpa | CpaLegacy | legacy_create_balance_info_v2 | LegacyCreateBalanceInfoV2Request | LegacyCreateBalanceInfoV2Response | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v2/callById | Звонок | да | cpa | CpaLegacy | legacy_create_call_by_id_v2 | LegacyCreateCallByIdV2Request | LegacyCreateCallByIdV2Response | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v2/callsByTime | Звонки по времени | нет | cpa | CpaCall | create_calls_by_time_v2 | CreateCallsByTimeV2Request | CreateCallsByTimeV2Response | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v2/chatsByTime | Чаты по времени | нет | cpa | CpaChat | create_chats_by_time | CreateChatsByTimeRequest | CreateChatsByTimeResponse | контракт+маппинг | | -| cpa | CPAАвито.json | POST | /cpa/v3/balanceInfo | Баланс | нет | cpa | CpaLead | create_balance_info_v3 | CreateBalanceInfoV3Request | CreateBalanceInfoV3Response | контракт+маппинг | | -| cpa | CallTracking[КТ].json | POST | /calltracking/v1/getCallById/ | Звонок по идентификатору | нет | cpa | CallTrackingCall | create_call_by_id | CreateCallByIdRequest | EmptyResponse | контракт+маппинг | | -| cpa | CallTracking[КТ].json | POST | /calltracking/v1/getCalls/ | Звонки по времени | нет | cpa | CallTrackingCall | create_calls | CreateCallsRequest | EmptyResponse | контракт+маппинг | | -| cpa | CallTracking[КТ].json | GET | /calltracking/v1/getRecordByCallId/ | Получение аудиозаписи звонка по идентификатору | нет | cpa | CallTrackingCall | get_record_by_call_id | NoRequest | EmptyResponse | контракт+маппинг | | -| promotion | TrxPromo.json | POST | /trx-promo/1/apply | Запуск продвижения | нет | promotion | TrxPromotion | create_trx_promo_open_api_apply | CreateTrxPromoOpenApiApplyRequest | CreateTrxPromoOpenApiApplyResponse | контракт+маппинг | | -| promotion | TrxPromo.json | POST | /trx-promo/1/cancel | Остановка продвижения | нет | promotion | TrxPromotion | delete_trx_promo_open_api_cancel | DeleteTrxPromoOpenApiCancelRequest | DeleteTrxPromoOpenApiCancelResponse | контракт+маппинг | | -| promotion | TrxPromo.json | GET | /trx-promo/1/commissions | Проверка доступности продвижения и размера комиссий | нет | promotion | TrxPromotion | get_trx_promo_open_api_commissions | GetTrxPromoOpenApiCommissionsRequest | GetTrxPromoOpenApiCommissionsResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v1/applications/apply_actions | Батчевая смена статуса откликов | нет | jobs | Application | get_applications_apply_actions | GetApplicationsApplyActionsRequest | GetApplicationsApplyActionsResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v1/applications/get_by_ids | Получение списка откликов | нет | jobs | Application | list_applications_get_by_ids | ListApplicationsGetByIdsRequest | ListApplicationsGetByIdsResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v1/applications/get_ids | Получение идентификаторов откликов | нет | jobs | Application | list_applications_get_ids | NoRequest | ListApplicationsGetIdsResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v1/applications/get_states | Получение списка возможных статусов откликов | нет | jobs | Application | list_applications_get_states | NoRequest | ListApplicationsGetStatesResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v1/applications/set_is_viewed | Изменение статуса отклика | нет | jobs | Application | get_applications_set_is_viewed | GetApplicationsSetIsViewedRequest | GetApplicationsSetIsViewedResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | DELETE | /job/v1/applications/webhook | Отключение уведомлений по откликам (webhook) | нет | jobs | JobWebhook | delete_applications_webhook_delete | NoRequest | DeleteApplicationsWebhookDeleteResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v1/applications/webhook | Получение информации о подписках (webhook) | нет | jobs | JobWebhook | get_applications_webhook_get | NoRequest | GetApplicationsWebhookGetResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | PUT | /job/v1/applications/webhook | Включение уведомлений по откликам (webhook) | нет | jobs | JobWebhook | update_applications_webhook_put | UpdateApplicationsWebhookPutRequest | UpdateApplicationsWebhookPutResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v1/applications/webhooks | Получение списка подписок (webhook) | нет | jobs | JobWebhook | list_applications_webhooks_get | NoRequest | ListApplicationsWebhooksGetResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v1/resumes/ | Поиск резюме | нет | jobs | Resume | list_resumes_get | NoRequest | ListResumesGetResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v1/resumes/{resume_id}/contacts/ | Доступ к контактным данным соискателя | нет | jobs | Resume | get_resume_get_contacts | NoRequest | GetResumeGetContactsResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v1/vacancies | Публикация вакансии | нет | jobs | Vacancy | create_vacancy_create | CreateVacancyCreateRequest | CreateVacancyCreateResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | PUT | /job/v1/vacancies/archived/{vacancy_id} | Остановка публикации вакансии | нет | jobs | Vacancy | delete_vacancy_archive | DeleteVacancyArchiveRequest | EmptyResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | PUT | /job/v1/vacancies/{vacancy_id} | Редактирование вакансии | нет | jobs | Vacancy | update_vacancy_update | UpdateVacancyUpdateRequest | EmptyResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v1/vacancies/{vacancy_id}/prolongate | Реактивация вакансии | нет | jobs | Vacancy | create_vacancy_prolongate | CreateVacancyProlongateRequest | EmptyResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v2/resumes/{resume_id} | Просмотр данных резюме | нет | jobs | Resume | get_resume_get_item | NoRequest | GetResumeGetItemResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v2/vacancies | Поиск вакансий | нет | jobs | Vacancy | list_search_vacancy | NoRequest | ListSearchVacancyResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v2/vacancies | Публикация вакансии v2 | нет | jobs | Vacancy | create_vacancy_create_v2 | CreateVacancyCreateV2Request | CreateVacancyCreateV2Response | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v2/vacancies/batch | Просмотр данных вакансий | нет | jobs | Vacancy | get_vacancies_get_by_ids | GetVacanciesGetByIdsRequest | GetVacanciesGetByIdsResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v2/vacancies/statuses | Получение статуса публикации вакансий V2 | нет | jobs | Vacancy | get_vacancy_get_statuses | GetVacancyGetStatusesRequest | GetVacancyGetStatusesResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | POST | /job/v2/vacancies/update/{vacancy_uuid} | Редактирование вакансии v2 | нет | jobs | Vacancy | update_vacancy_update_v2 | UpdateVacancyUpdateV2Request | UpdateVacancyUpdateV2Response | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v2/vacancies/{vacancy_id} | Просмотр данных вакансии | нет | jobs | Vacancy | get_vacancy_get_item | NoRequest | GetVacancyGetItemResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | PUT | /job/v2/vacancies/{vacancy_uuid}/auto_renewal | Автопродление вакансии v2 | нет | jobs | Vacancy | update_vacancy_auto_renewal | UpdateVacancyAutoRenewalRequest | EmptyResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v2/vacancy/dict | Получение списка доступных словарей | нет | jobs | JobDictionary | list_dicts | NoRequest | ListDictsResponse | контракт+маппинг | | -| jobs | АвитоРабота.json | GET | /job/v2/vacancy/dict/{dictionary_id} | Получение доступных значений списка по ID словаря | нет | jobs | JobDictionary | list_dict_by_id | NoRequest | ListDictByIdResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v1/profile | Получение профиля пользователя автозагрузки (deprecated) | да | ads | AutoloadLegacy | legacy_get_profile | NoRequest | LegacyGetProfileResponse | контракт+маппинг | | -| ads | Автозагрузка.json | POST | /autoload/v1/profile | Создание/редактирование настроек профиля пользователя автозагрузки (deprecated) | да | ads | AutoloadLegacy | legacy_create_or_update_profile | LegacyCreateOrUpdateProfileRequest | EmptyResponse | контракт+маппинг | | -| ads | Автозагрузка.json | POST | /autoload/v1/upload | Загрузка файла по ссылке | нет | ads | AutoloadProfile | create_upload | NoRequest | EmptyResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v1/user-docs/node/{node_slug}/fields | Получения полей категории | нет | ads | AutoloadProfile | get_user_docs_node_fields | NoRequest | GetUserDocsNodeFieldsResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v1/user-docs/tree | Получение дерева категорий | нет | ads | AutoloadProfile | get_user_docs_tree | NoRequest | GetUserDocsTreeResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/items/ad_ids | ID объявлений из файла | нет | ads | AutoloadReport | get_ad_ids_by_avito_ids | NoRequest | GetAdIdsByAvitoIdsResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/items/avito_ids | ID объявлений на Авито | нет | ads | AutoloadReport | get_avito_ids_by_ad_ids | NoRequest | GetAvitoIdsByAdIdsResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/profile | Получение профиля пользователя автозагрузки | нет | ads | AutoloadProfile | get_profile_v2 | NoRequest | GetProfileV2Response | контракт+маппинг | | -| ads | Автозагрузка.json | POST | /autoload/v2/profile | Создание/редактирование настроек профиля пользователя автозагрузки | нет | ads | AutoloadProfile | create_or_update_profile_v2 | CreateOrUpdateProfileV2Request | EmptyResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/reports | Список отчётов автозагрузки | нет | ads | AutoloadReport | list_reports_v2 | NoRequest | ListReportsV2Response | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/reports/items | Объявления по ID в автозагрузке | нет | ads | AutoloadReport | get_autoload_items_info_v2 | NoRequest | GetAutoloadItemsInfoV2Response | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/reports/last_completed_report | Статистика по последней выгрузке (deprecated) | да | ads | AutoloadLegacy | legacy_get_last_completed_report | NoRequest | LegacyGetLastCompletedReportResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/reports/{report_id} | Статистика по конкретной выгрузке (deprecated) | да | ads | AutoloadLegacy | legacy_get_report_by_id_v2 | NoRequest | LegacyGetReportByIdV2Response | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/reports/{report_id}/items | Все объявления из конкретной выгрузки | нет | ads | AutoloadReport | get_report_items_by_id | NoRequest | GetReportItemsByIdResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v2/reports/{report_id}/items/fees | Списания за объявления в конкретной выгрузке | нет | ads | AutoloadReport | get_report_items_fees_by_id | NoRequest | GetReportItemsFeesByIdResponse | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v3/reports/last_completed_report | Статистика по последней выгрузке | нет | ads | AutoloadReport | get_last_completed_report_v3 | NoRequest | GetLastCompletedReportV3Response | контракт+маппинг | | -| ads | Автозагрузка.json | GET | /autoload/v3/reports/{report_id} | Статистика по конкретной выгрузке | нет | ads | AutoloadReport | get_report_by_id_v3 | NoRequest | GetReportByIdV3Response | контракт+маппинг | | -| auth | Авторизация.json | POST | /token | Получение access token | нет | auth | AvitoClient.auth() | get_access_token | GetAccessTokenRequest | GetAccessTokenResponse | контракт+маппинг | канонический token-endpoint | -| auth | Авторизация.json | POST | /token | Получение access token | нет | auth | AvitoClient.auth() | get_access_token_authorization_code | GetAccessTokenAuthorizationCodeRequest | GetAccessTokenAuthorizationCodeResponse | контракт+маппинг | нормализованы скрытые Unicode-символы в пути /token для inventory | -| auth | Авторизация.json | POST | /token | Обновление access token | нет | auth | AvitoClient.auth() | update_refresh_access_token_authorization_code | UpdateRefreshAccessTokenAuthorizationCodeRequest | UpdateRefreshAccessTokenAuthorizationCodeResponse | контракт+маппинг | нормализованы скрытые Unicode-символы в пути /token для inventory | -| promotion | Автостратегия.json | POST | /autostrategy/v1/budget | Расчет бюджета кампании | нет | promotion | AutostrategyCampaign | create_autostrategy_budget | CreateAutostrategyBudgetRequest | CreateAutostrategyBudgetResponse | контракт+маппинг | | -| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/create | Создание новой кампании | нет | promotion | AutostrategyCampaign | create_autostrategy_campaign | CreateAutostrategyCampaignRequest | CreateAutostrategyCampaignResponse | контракт+маппинг | | -| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/edit | Редактирование кампании | нет | promotion | AutostrategyCampaign | update_edit_autostrategy_campaign | UpdateEditAutostrategyCampaignRequest | UpdateEditAutostrategyCampaignResponse | контракт+маппинг | | -| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/info | Получение полной информации о кампании | нет | promotion | AutostrategyCampaign | get_autostrategy_campaign_info | GetAutostrategyCampaignInfoRequest | GetAutostrategyCampaignInfoResponse | контракт+маппинг | | -| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/stop | Остановка кампании | нет | promotion | AutostrategyCampaign | delete_stop_autostrategy_campaign | DeleteStopAutostrategyCampaignRequest | DeleteStopAutostrategyCampaignResponse | контракт+маппинг | | -| promotion | Автостратегия.json | POST | /autostrategy/v1/campaigns | Получение списка кампаний | нет | promotion | AutostrategyCampaign | list_autostrategy_campaigns | ListAutostrategyCampaignsRequest | ListAutostrategyCampaignsResponse | контракт+маппинг | | -| promotion | Автостратегия.json | POST | /autostrategy/v1/stat | Получение статистики по кампании | нет | promotion | AutostrategyCampaign | get_autostrategy_stat | GetAutostrategyStatRequest | GetAutostrategyStatResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/catalogs/resolve | Получение актуальных параметров Автокаталога | нет | autoteka | AutotekaVehicle | get_catalogs_resolve | GetCatalogsResolveRequest | GetCatalogsResolveResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/get-leads/ | Получение событий сервиса Сигнал | нет | autoteka | AutotekaVehicle | get_leads | GetLeadsRequest | GetLeadsResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/monitoring/bucket/add | Добавить идентификаторы (vin/frame) на мониторинг | нет | autoteka | AutotekaMonitoring | create_monitoring_bucket_add | CreateMonitoringBucketAddRequest | CreateMonitoringBucketAddResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/monitoring/bucket/delete | Полная очистка списка мониторинга | нет | autoteka | AutotekaMonitoring | list_monitoring_bucket_delete | NoRequest | ListMonitoringBucketDeleteResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/monitoring/bucket/remove | Удаление идентификаторов из мониторинга (vin/frame) | нет | autoteka | AutotekaMonitoring | delete_monitoring_bucket_remove | DeleteMonitoringBucketRemoveRequest | DeleteMonitoringBucketRemoveResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/monitoring/get-reg-actions/ | Получение событий мониторинга | нет | autoteka | AutotekaMonitoring | get_monitoring_get_reg_actions | NoRequest | GetMonitoringGetRegActionsResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/packages/active_package | Запрос остатка отчётов пользователя | нет | autoteka | AutotekaReport | get_active_package | NoRequest | GetActivePackageResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/previews | Превью по VIN или номеру кузова | нет | autoteka | AutotekaVehicle | create_preview_by_vin | CreatePreviewByVinRequest | CreatePreviewByVinResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/previews/{previewId} | Получение превью по его ID | нет | autoteka | AutotekaVehicle | get_preview | NoRequest | GetPreviewResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/reports | Отчет по превью | нет | autoteka | AutotekaReport | create_report | CreateReportRequest | CreateReportResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/reports-by-vehicle-id | Отчет по идентификатору авто (vin/frame) | нет | autoteka | AutotekaReport | create_report_by_vehicle_id | CreateReportByVehicleIdRequest | CreateReportByVehicleIdResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/reports/list/ | Получение списка отчётов | нет | autoteka | AutotekaReport | list_report_list | NoRequest | ListReportListResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/reports/{report_id} | Получение отчета по его ID | нет | autoteka | AutotekaReport | get_report | NoRequest | GetReportResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/request-preview-by-external-item | Превью по ID объявления другой площадки | нет | autoteka | AutotekaVehicle | create_preview_by_external_item | CreatePreviewByExternalItemRequest | CreatePreviewByExternalItemResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/request-preview-by-item-id | Превью по ID объявления Авито | нет | autoteka | AutotekaVehicle | create_preview_by_item_id | CreatePreviewByItemIdRequest | CreatePreviewByItemIdResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/request-preview-by-regnumber | Превью по государственному номеру | нет | autoteka | AutotekaVehicle | create_preview_by_reg_number | CreatePreviewByRegNumberRequest | CreatePreviewByRegNumberResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/scoring/by-vehicle-id | Скоринг рисков по идентификатору авто (vin/frame) | нет | autoteka | AutotekaScoring | create_scoring_by_vehicle_id | CreateScoringByVehicleIdRequest | CreateScoringByVehicleIdResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/scoring/{scoring_id} | Получение скоринга рисков по его ID | нет | autoteka | AutotekaScoring | get_scoring_get_by_id | NoRequest | GetScoringGetByIdResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/specifications/by-plate-number | Запрос характеристик по регистрационному номеру | нет | autoteka | AutotekaVehicle | create_specification_by_plate_number | CreateSpecificationByPlateNumberRequest | CreateSpecificationByPlateNumberResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/specifications/by-vehicle-id | Запрос характеристик по идентификатору авто (vin/frame) | нет | autoteka | AutotekaVehicle | create_specification_by_vehicle_id | CreateSpecificationByVehicleIdRequest | CreateSpecificationByVehicleIdResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/specifications/specification/{specificationID} | Получение характеристик по ID запроса | нет | autoteka | AutotekaVehicle | get_specification_get_by_id | NoRequest | GetSpecificationGetByIdResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/sync/create-by-regnumber | Синхронное создание отчета по ГРЗ | нет | autoteka | AutotekaReport | create_sync_create_report_by_reg_number | CreateSyncCreateReportByRegNumberRequest | CreateSyncCreateReportByRegNumberResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/sync/create-by-vin | Синхронное создание отчёта по VIN или номеру кузова | нет | autoteka | AutotekaReport | create_sync_create_report_by_vin | CreateSyncCreateReportByVinRequest | CreateSyncCreateReportByVinResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/teasers | Тизер по идентификатору авто (vin/frame) | нет | autoteka | AutotekaVehicle | create_teaser | CreateTeaserRequest | CreateTeaserResponse | контракт+маппинг | | -| autoteka | Автотека.json | GET | /autoteka/v1/teasers/{teaser_id} | Получение тизера по ID тизера | нет | autoteka | AutotekaVehicle | get_teaser | NoRequest | GetTeaserResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /autoteka/v1/valuation/by-specification | Получение оценки по параметрам | нет | autoteka | AutotekaValuation | get_valuation_by_specification | GetValuationBySpecificationRequest | GetValuationBySpecificationResponse | контракт+маппинг | | -| autoteka | Автотека.json | POST | /token | Получение access token | нет | autoteka | AvitoClient.auth() | get_access_token | NoRequest | GetAccessTokenResponse | контракт+маппинг | | -| realty | Аналитикапонедвижимости.json | GET | /realty/v1/marketPriceCorrespondence/{itemId}/{price} | Получение соответствия переданной цены рыночной цене | нет | realty | RealtyAnalyticsReport | get_market_price_correspondence_v1 | NoRequest | GetMarketPriceCorrespondenceV1Response | контракт+маппинг | | -| realty | Аналитикапонедвижимости.json | POST | /realty/v1/report/create/{itemId} | Получение аналитического отчета по недвижимости | нет | realty | RealtyAnalyticsReport | get_report_for_classified | NoRequest | GetReportForClassifiedResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /cancelAnnouncement | Отмена анонса в СД | нет | orders | DeliveryOrder | delete_cancel_announcement3_pl | DeleteCancelAnnouncement3PlRequest | DeleteCancelAnnouncement3PlResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /createAnnouncement | Создание анонса в СД | нет | orders | DeliveryOrder | create_announcement3_pl | CreateAnnouncement3PlRequest | CreateAnnouncement3PlResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /createParcel | Создание посылки | нет | orders | DeliveryOrder | create_parcel | CreateParcelRequest | CreateParcelResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/announcements/create | Создание анонса в Avito | нет | orders | SandboxDelivery | create_announcement | CreateAnnouncementRequest | CreateAnnouncementResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/announcements/track | Трекинг анонсов | нет | orders | SandboxDelivery | create_track_announcement | CreateTrackAnnouncementRequest | CreateTrackAnnouncementResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/areas/custom-schedule | Установка графика работы на определённый день | нет | orders | SandboxDelivery | update_custom_area_schedule | UpdateCustomAreaScheduleRequest | UpdateCustomAreaScheduleResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/cancelParcel | Отмена посылки | нет | orders | SandboxDelivery | delete_cancel_parcel | DeleteCancelParcelRequest | DeleteCancelParcelResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/order/checkConfirmationCode | Проверка кода подтверждения | нет | orders | SandboxDelivery | get_check_confirmation_code | GetCheckConfirmationCodeRequest | GetCheckConfirmationCodeResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/order/properties | Добавление / изменение параметров доставки посылки | нет | orders | SandboxDelivery | create_set_order_properties | CreateSetOrderPropertiesRequest | CreateSetOrderPropertiesResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/order/realAddress | Фактический адрес приёма / возврата посылки | нет | orders | SandboxDelivery | create_set_order_real_address | CreateSetOrderRealAddressRequest | CreateSetOrderRealAddressResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/order/tracking | Трекинг | нет | orders | SandboxDelivery | create_tracking | CreateTrackingRequest | CreateTrackingResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/prohibitOrderAcceptance | Запрет приёма посылки от отправителя | нет | orders | SandboxDelivery | delete_prohibit_order_acceptance | DeleteProhibitOrderAcceptanceRequest | DeleteProhibitOrderAcceptanceResponse | контракт+маппинг | | -| orders | Доставка.json | GET | /delivery-sandbox/sorting-center | Получить список сортировочных центров | нет | orders | SandboxDelivery | list_sorting_center | NoRequest | ListSortingCenterResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/tariffs/sorting-center | Загрузить сортировочные центры | нет | orders | SandboxDelivery | create_add_sorting_center | CreateAddSortingCenterRequest | CreateAddSortingCenterResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/areas | Загрузить области доставки | нет | orders | SandboxDelivery | create_add_areas_sandbox | CreateAddAreasSandboxRequest | CreateAddAreasSandboxResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/tagged-sorting-centers | Установка тэгов своим и/или чужим сортировочным центрам | нет | orders | SandboxDelivery | update_add_tags_to_sorting_center | UpdateAddTagsToSortingCenterRequest | UpdateAddTagsToSortingCenterResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/terminals | Загрузить терминалы | нет | orders | SandboxDelivery | create_add_terminals_sandbox | CreateAddTerminalsSandboxRequest | CreateAddTerminalsSandboxResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/terms | Обновить сроки по тарифу | нет | orders | SandboxDelivery | update_update_terms | UpdateUpdateTermsRequest | UpdateUpdateTermsResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/tariffsV2 | Загрузить новый тариф v2 | нет | orders | SandboxDelivery | create_add_tariff_sandbox_v2 | CreateAddTariffSandboxV2Request | CreateAddTariffSandboxV2Response | контракт+маппинг | | -| orders | Доставка.json | GET | /delivery-sandbox/tasks/{task_id} | Получение информации по задаче | нет | orders | DeliveryTask | get_task | NoRequest | GetTaskResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/cancelAnnouncement | Отправка события об отмене тестового анонса | нет | orders | SandboxDelivery | create_v1cancel_announcement | CreateV1cancelAnnouncementRequest | CreateV1cancelAnnouncementResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/cancelParcel | Отмена тестовой посылки | нет | orders | SandboxDelivery | delete_v1_cancel_parcel | DeleteV1CancelParcelRequest | DeleteV1CancelParcelResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/changeParcel | Создание заявки на изменение данных тестовой посылки | нет | orders | SandboxDelivery | create_v1change_parcel | CreateV1changeParcelRequest | CreateV1changeParcelResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/createAnnouncement | Создание тестового анонса | нет | orders | SandboxDelivery | create_v1create_announcement | CreateV1createAnnouncementRequest | CreateV1createAnnouncementResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/getAnnouncementEvent | Получение последнего события тестового анонса | нет | orders | SandboxDelivery | get_v1get_announcement_event | GetV1getAnnouncementEventRequest | GetV1getAnnouncementEventResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/getChangeParcelInfo | Получение информации об изменении тестовой посылки | нет | orders | SandboxDelivery | get_v1get_change_parcel_info | GetV1getChangeParcelInfoRequest | GetV1getChangeParcelInfoResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/getParcelInfo | Получение информации о тестовой посылке | нет | orders | SandboxDelivery | get_v1get_parcel_info | GetV1getParcelInfoRequest | GetV1getParcelInfoResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v1/getRegisteredParcelID | Получение ID зарегистрированной тестовой посылки | нет | orders | SandboxDelivery | get_v1get_registered_parcel_id | GetV1getRegisteredParcelIdRequest | GetV1getRegisteredParcelIdResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery-sandbox/v2/createParcel | Создание тестовой посылки | нет | orders | SandboxDelivery | create_sandbox_parcel_v2 | CreateSandboxParcelV2Request | CreateSandboxParcelV2Response | контракт+маппинг | | -| orders | Доставка.json | POST | /delivery/order/changeParcelResult | Отправка результата исполнения заявки | нет | orders | DeliveryOrder | create_change_parcel_result | CreateChangeParcelResultRequest | CreateChangeParcelResultResponse | контракт+маппинг | | -| orders | Доставка.json | POST | /sandbox/changeParcels | Обновление свойств посылок | нет | orders | DeliveryOrder | update_change_parcels | UpdateChangeParcelsRequest | UpdateChangeParcelsResponse | контракт+маппинг | | -| accounts | ИерархияАккаунтов.json | GET | /checkAhUserV1 | Получение информации о статусе пользователя в ИА | нет | accounts | AccountHierarchy | get_check_ah_user_v1 | NoRequest | GetCheckAhUserV1Response | контракт+маппинг | | -| accounts | ИерархияАккаунтов.json | GET | /getEmployeesV1 | Получение списка сотрудников иерархии | нет | accounts | AccountHierarchy | list_employees_v1 | NoRequest | ListEmployeesV1Response | контракт+маппинг | | -| accounts | ИерархияАккаунтов.json | POST | /linkItemsV1 | Прикрепление сотрудника иерархии к объявлениям, перезакрепление объявлений между сотрудниками иерархии | нет | accounts | AccountHierarchy | create_link_items_v1 | CreateLinkItemsV1Request | EmptyResponse | контракт+маппинг | | -| accounts | ИерархияАккаунтов.json | GET | /listCompanyPhonesV1 | Получение списка телефонов компании | нет | accounts | AccountHierarchy | list_company_phones_v1 | NoRequest | ListCompanyPhonesV1Response | контракт+маппинг | | -| accounts | ИерархияАккаунтов.json | POST | /listItemsByEmployeeIdV1 | Получение списка объявлений по сотруднику | нет | accounts | AccountHierarchy | list_items_by_employee_id_v1 | ListItemsByEmployeeIdV1Request | ListItemsByEmployeeIdV1Response | контракт+маппинг | | -| accounts | Информацияопользователе.json | POST | /core/v1/accounts/operations_history/ | Получение истории операций пользователя | нет | accounts | Account | get_operations_history | GetOperationsHistoryRequest | GetOperationsHistoryResponse | контракт+маппинг | | -| accounts | Информацияопользователе.json | GET | /core/v1/accounts/self | Получение информации об авторизованном пользователе | нет | accounts | Account | get_user_info_self | NoRequest | GetUserInfoSelfResponse | контракт+маппинг | | -| accounts | Информацияопользователе.json | GET | /core/v1/accounts/{user_id}/balance/ | Получение баланса кошелька пользователя | нет | accounts | Account | get_user_balance | NoRequest | GetUserBalanceResponse | контракт+маппинг | | -| realty | Краткосрочнаяаренда.json | POST | /core/v1/accounts/{user_id}/items/{item_id}/bookings | Заполнение календаря занятости объекта недвижимости | нет | realty | RealtyBooking | update_bookings_info | UpdateBookingsInfoRequest | UpdateBookingsInfoResponse | контракт+маппинг | | -| realty | Краткосрочнаяаренда.json | GET | /realty/v1/accounts/{user_id}/items/{item_id}/bookings | Получение списка броней по объявлению | нет | realty | RealtyBooking | list_realty_bookings | NoRequest | ListRealtyBookingsResponse | контракт+маппинг | | -| realty | Краткосрочнаяаренда.json | POST | /realty/v1/accounts/{user_id}/items/{item_id}/prices | Актуализация параметров для выбранных периодов | нет | realty | RealtyPricing | update_realty_prices | UpdateRealtyPricesRequest | UpdateRealtyPricesResponse | контракт+маппинг | | -| realty | Краткосрочнаяаренда.json | POST | /realty/v1/items/intervals | Заполнение доступности объекта недвижимости с квотами и без | нет | realty | RealtyListing | get_intervals | GetIntervalsRequest | GetIntervalsResponse | контракт+маппинг | | -| realty | Краткосрочнаяаренда.json | POST | /realty/v1/items/{item_id}/base | Установка базовых параметров | нет | realty | RealtyListing | update_base_params | UpdateBaseParamsRequest | EmptyResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/messages | Отправка сообщения | нет | messenger | ChatMessage | create_send_message | CreateSendMessageRequest | CreateSendMessageResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/messages/image | Отправка сообщения с изображением | нет | messenger | ChatMessage | create_send_image_message | CreateSendImageMessageRequest | CreateSendImageMessageResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/messages/{message_id} | Удаление сообщения | нет | messenger | ChatMessage | delete_message | NoRequest | DeleteMessageResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/read | Прочитать чат | нет | messenger | Chat | create_chat_read | NoRequest | CreateChatReadResponse | контракт+маппинг | | -| messenger | Мессенджер.json | GET | /messenger/v1/accounts/{user_id}/getVoiceFiles | Получение голосовых сообщений | нет | messenger | ChatMedia | get_voice_files | NoRequest | GetVoiceFilesResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/uploadImages | Загрузка изображений | нет | messenger | ChatMedia | create_upload_images | CreateUploadImagesMultipartRequest | CreateUploadImagesResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v1/subscriptions | Получение подписок (webhooks) | нет | messenger | ChatWebhook | get_subscriptions | NoRequest | GetSubscriptionsResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v1/webhook/unsubscribe | Отключение уведомлений (webhooks) | нет | messenger | ChatWebhook | delete_webhook_unsubscribe | DeleteWebhookUnsubscribeRequest | DeleteWebhookUnsubscribeResponse | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v2/accounts/{user_id}/blacklist | Добавление пользователя в blacklist | нет | messenger | Chat | create_blacklist_v2 | CreateBlacklistV2Request | EmptyResponse | контракт+маппинг | | -| messenger | Мессенджер.json | GET | /messenger/v2/accounts/{user_id}/chats | Получение информации по чатам | нет | messenger | Chat | get_chats_v2 | NoRequest | GetChatsV2Response | контракт+маппинг | | -| messenger | Мессенджер.json | GET | /messenger/v2/accounts/{user_id}/chats/{chat_id} | Получение информации по чату | нет | messenger | Chat | get_chat_by_id_v2 | NoRequest | GetChatByIdV2Response | контракт+маппинг | | -| messenger | Мессенджер.json | GET | /messenger/v3/accounts/{user_id}/chats/{chat_id}/messages/ | Получение списка сообщений V3 | нет | messenger | ChatMessage | list_messages_v3 | NoRequest | ListMessagesV3Response | контракт+маппинг | | -| messenger | Мессенджер.json | POST | /messenger/v3/webhook | Включение уведомлений V3 (webhooks) | нет | messenger | ChatWebhook | update_webhook_v3 | UpdateWebhookV3Request | UpdateWebhookV3Response | контракт+маппинг | | -| promotion | Настройкаценыцелевогодействия.json | GET | /cpxpromo/1/getBids/{itemId} | Получение детализированной информации о действующих и доступных ценах за целевые действия и бюджетах | нет | promotion | TargetActionPricing | get_bids | NoRequest | GetBidsResponse | контракт+маппинг | | -| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/getPromotionsByItemIds | Получение текущих цен за целевое действие и бюджетов по нескольким объявлениям | нет | promotion | TargetActionPricing | get_promotions_by_item_ids | GetPromotionsByItemIdsRequest | GetPromotionsByItemIdsResponse | контракт+маппинг | | -| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/remove | Остановка продвижения | нет | promotion | TargetActionPricing | delete_promotion | DeletePromotionRequest | DeletePromotionResponse | контракт+маппинг | | -| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/setAuto | Применение автоматической настройки | нет | promotion | TargetActionPricing | update_auto_bid | UpdateAutoBidRequest | EmptyResponse | контракт+маппинг | | -| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/setManual | Применение ручной настройки | нет | promotion | TargetActionPricing | update_manual_bid | UpdateManualBidRequest | EmptyResponse | контракт+маппинг | | -| ads | Объявления.json | POST | /core/v1/accounts/{userId}/vas/prices | Получение информации о стоимости услуг продвижения и доступных значках | нет | ads | AdPromotion | get_vas_prices | GetVasPricesRequest | GetVasPricesResponse | контракт+маппинг | | -| ads | Объявления.json | POST | /core/v1/accounts/{user_id}/calls/stats/ | Получение статистики по звонкам | нет | ads | AdStats | get_calls_stats | GetCallsStatsRequest | GetCallsStatsResponse | контракт+маппинг | | -| ads | Объявления.json | GET | /core/v1/accounts/{user_id}/items/{item_id}/ | Получение информации по объявлению | нет | ads | Ad | get_item_info | NoRequest | GetItemInfoResponse | контракт+маппинг | | -| ads | Объявления.json | PUT | /core/v1/accounts/{user_id}/items/{item_id}/vas | Применение дополнительных услуг | нет | ads | AdPromotion | update_item_vas | UpdateItemVasRequest | UpdateItemVasResponse | контракт+маппинг | | -| ads | Объявления.json | GET | /core/v1/items | Получение информации по объявлениям | нет | ads | Ad | get_items_info | NoRequest | GetItemsInfoResponse | контракт+маппинг | | -| ads | Объявления.json | POST | /core/v1/items/{item_id}/update_price | Обновление цены объявления | нет | ads | Ad | update_update_price | UpdateUpdatePriceRequest | UpdateUpdatePriceResponse | контракт+маппинг | | -| ads | Объявления.json | PUT | /core/v2/accounts/{user_id}/items/{item_id}/vas_packages | Применение пакета дополнительных услуг | нет | ads | AdPromotion | update_item_vas_package_v2 | UpdateItemVasPackageV2Request | UpdateItemVasPackageV2Response | контракт+маппинг | | -| ads | Объявления.json | PUT | /core/v2/items/{itemId}/vas/ | Применение услуг продвижения | нет | ads | AdPromotion | update_apply_vas | UpdateApplyVasRequest | UpdateApplyVasResponse | контракт+маппинг | | -| ads | Объявления.json | POST | /stats/v1/accounts/{user_id}/items | Получение статистики по списку объявлений | нет | ads | AdStats | get_item_stats_shallow | GetItemStatsShallowRequest | GetItemStatsShallowResponse | контракт+маппинг | | -| ads | Объявления.json | POST | /stats/v2/accounts/{user_id}/items | Получение статистических показателей по профилю | нет | ads | AdStats | get_item_analytics | GetItemAnalyticsRequest | GetItemAnalyticsResponse | контракт+маппинг | | -| ads | Объявления.json | POST | /stats/v2/accounts/{user_id}/spendings | Получение статистики расходов профиля | нет | ads | AdStats | get_account_spendings | GetAccountSpendingsRequest | GetAccountSpendingsResponse | контракт+маппинг | | -| promotion | Продвижение.json | POST | /promotion/v1/items/services/bbip/forecasts/get | BBIP. Прогноз продвижения | нет | promotion | BbipPromotion | create_bbip_forecasts_by_items_v1 | CreateBbipForecastsByItemsV1Request | CreateBbipForecastsByItemsV1Response | контракт+маппинг | | -| promotion | Продвижение.json | PUT | /promotion/v1/items/services/bbip/orders/create | BBIP. Подключение услуги продвижения | нет | promotion | BbipPromotion | update_bbip_order_for_items_v1 | UpdateBbipOrderForItemsV1Request | UpdateBbipOrderForItemsV1Response | контракт+маппинг | | -| promotion | Продвижение.json | POST | /promotion/v1/items/services/bbip/suggests/get | BBIP. Варианты бюджета продвижения | нет | promotion | BbipPromotion | create_bbip_suggests_by_items_v1 | CreateBbipSuggestsByItemsV1Request | CreateBbipSuggestsByItemsV1Response | контракт+маппинг | | -| promotion | Продвижение.json | POST | /promotion/v1/items/services/dict | Словарь типов услуг продвижения | нет | promotion | PromotionOrder | create_dict_of_services_v1 | NoRequest | CreateDictOfServicesV1Response | контракт+маппинг | | -| promotion | Продвижение.json | POST | /promotion/v1/items/services/get | Список услуг продвижения | нет | promotion | PromotionOrder | list_services_by_items_v1 | ListServicesByItemsV1Request | ListServicesByItemsV1Response | контракт+маппинг | | -| promotion | Продвижение.json | POST | /promotion/v1/items/services/orders/get | Список заявок | нет | promotion | PromotionOrder | list_orders_by_user_v1 | ListOrdersByUserV1Request | ListOrdersByUserV1Response | контракт+маппинг | | -| promotion | Продвижение.json | POST | /promotion/v1/items/services/orders/status | Статус заявки | нет | promotion | PromotionOrder | get_order_status_v1 | GetOrderStatusV1Request | GetOrderStatusV1Response | контракт+маппинг | | -| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/available | Получение информации об объявлениях | нет | messenger | SpecialOfferCampaign | get_available | GetAvailableRequest | GetAvailableResponse | контракт+маппинг | | -| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/multiConfirm | Отправка и оплата рассылки | нет | messenger | SpecialOfferCampaign | create_multi_confirm | CreateMultiConfirmRequest | CreateMultiConfirmResponse | контракт+маппинг | | -| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/multiCreate | Создание рассылки | нет | messenger | SpecialOfferCampaign | create_multi_create | CreateMultiCreateRequest | CreateMultiCreateResponse | контракт+маппинг | | -| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/stats | Получение статистики | нет | messenger | SpecialOfferCampaign | get_stats | GetStatsRequest | GetStatsResponse | контракт+маппинг | | -| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/tariffInfo | Получение информации о тарифе | нет | messenger | SpecialOfferCampaign | get_tariff_info | NoRequest | GetTariffInfoResponse | контракт+маппинг | | -| ratings | Рейтингииотзывы.json | POST | /ratings/v1/answers | Отправка ответа на отзыв | нет | ratings | ReviewAnswer | create_review_answer_v1 | CreateReviewAnswerV1Request | CreateReviewAnswerV1Response | контракт+маппинг | | -| ratings | Рейтингииотзывы.json | DELETE | /ratings/v1/answers/{answer_id} | Запрос на удаление ответа на отзыв | нет | ratings | ReviewAnswer | delete_review_answer_v1 | NoRequest | DeleteReviewAnswerV1Response | контракт+маппинг | | -| ratings | Рейтингииотзывы.json | GET | /ratings/v1/info | Получение информации о рейтинге пользователя | нет | ratings | RatingProfile | get_ratings_info_v1 | NoRequest | GetRatingsInfoV1Response | контракт+маппинг | | -| ratings | Рейтингииотзывы.json | GET | /ratings/v1/reviews | Получение списка активных отзывов на пользователя с пагинацией | нет | ratings | Review | list_reviews_v1 | NoRequest | ListReviewsV1Response | контракт+маппинг | | -| tariffs | Тарифы.json | GET | /tariff/info/1 | Информация по тарифу | нет | tariffs | Tariff | get_tariff_info | NoRequest | GetTariffInfoResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/markings | Передача честного знака | нет | orders | Order | update_markings | UpdateMarkingsRequest | UpdateMarkingsResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/order/acceptReturnOrder | Выбор отделения отделения Почты России для получения возврата | нет | orders | Order | create_accept_return_order | CreateAcceptReturnOrderRequest | CreateAcceptReturnOrderResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/order/applyTransition | Изменение статуса заказа | нет | orders | Order | get_apply_transition | GetApplyTransitionRequest | GetApplyTransitionResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/order/checkConfirmationCode | Метод для проверки кода подтверждения заказа. | нет | orders | Order | create_check_confirmation_code | CreateCheckConfirmationCodeRequest | CreateCheckConfirmationCodeResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/order/cncSetDetails | Метод для подготовки заказа с самовывозом | нет | orders | Order | create_cnc_set_details | CreateCncSetDetailsRequest | CreateCncSetDetailsResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | GET | /order-management/1/order/getCourierDeliveryRange | Метод получения доступных временных промежутков приезда курьера | нет | orders | Order | get_courier_delivery_range | NoRequest | GetCourierDeliveryRangeResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/order/setCourierDeliveryRange | Метод выбора определённого доступного временного промежутка для приезда курьера | нет | orders | Order | get_set_courier_delivery_range | GetSetCourierDeliveryRangeRequest | GetSetCourierDeliveryRangeResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/order/setTrackingNumber | Передача трек-номера | нет | orders | Order | update_set_order_tracking_number | UpdateSetOrderTrackingNumberRequest | UpdateSetOrderTrackingNumberResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | GET | /order-management/1/orders | Получение информации о заказах | нет | orders | Order | get_orders | NoRequest | GetOrdersResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/orders/labels | Создать задачу на генерацию этикеток (до 100). | нет | orders | OrderLabel | create_generate_labels | CreateGenerateLabelsRequest | CreateGenerateLabelsResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | POST | /order-management/1/orders/labels/extended | Создать задачу на генерацию этикеток (до 1000). | нет | orders | OrderLabel | create_generate_labels_extended | CreateGenerateLabelsExtendedRequest | CreateGenerateLabelsExtendedResponse | контракт+маппинг | | -| orders | Управлениезаказами.json | GET | /order-management/1/orders/labels/{taskID}/download | Скачать сгенерированный PDF-файл (этикетку). | нет | orders | OrderLabel | get_download_label | NoRequest | BinaryPdfResponse | контракт+бинарный | | -| orders | Управлениеостатками.json | POST | /stock-management/1/info | Получение остатков | нет | orders | Stock | get_получение_остатков | GetПолучениеОстатковRequest | GetПолучениеОстатковResponse | контракт+маппинг | в swagger отсутствует operationId | -| orders | Управлениеостатками.json | PUT | /stock-management/1/stocks | Редактирование остатков | нет | orders | Stock | update_редактирование_остатков | UpdateРедактированиеОстатковRequest | UpdateРедактированиеОстатковResponse | контракт+маппинг | в swagger отсутствует operationId | +| раздел | документ | метод | путь | описание | deprecated | deprecated_since | replacement | removal_version | пакет_sdk | доменный_объект | публичный_метод_sdk | тип_запроса | тип_ответа | тип_теста | примечания | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| promotion | CPA-аукцион.json | GET | /auction/1/bids | Получение информации о действующих и доступных ставках | нет | | | | promotion | CpaAuction | get_user_bids | NoRequest | GetUserBidsResponse | контракт+маппинг | | +| promotion | CPA-аукцион.json | POST | /auction/1/bids | Сохранение новых ставок | нет | | | | promotion | CpaAuction | create_item_bids | CreateItemBidsRequest | EmptyResponse | контракт+маппинг | | +| cpa | CPAАвито.json | GET | /cpa/v1/call/{call_id} | Запись звонка (deprecated) | да | 1.1.0 | call_tracking_call().download | 1.3.0 | cpa | CpaArchive | get_call | NoRequest | EmptyResponse | контракт+маппинг | | +| cpa | CPAАвито.json | GET | /cpa/v1/chatByActionId/{actionId} | Чат | нет | | | | cpa | CpaChat | get_chat_by_action_id | NoRequest | GetChatByActionIdResponse | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v1/chatsByTime | Чаты по времени (deprecated) | да | 1.1.0 | cpa_chat().list(version=2) | 1.3.0 | cpa | CpaChat | list | CreateChatsByTimeRequest | CreateChatsByTimeResponse | контракт+маппинг | deprecated при version=1 | +| cpa | CPAАвито.json | POST | /cpa/v1/createComplaint | Создание жалобы для звонков | нет | | | | cpa | CpaCall | create_create_complaint | CreateCreateComplaintRequest | CreateCreateComplaintResponse | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v1/createComplaintByActionId | Создание жалобы для звонков/чатов | нет | | | | cpa | CpaLead | create_complaint_by_action_id | CreateComplaintByActionIdRequest | CreateComplaintByActionIdResponse | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v1/phonesInfoFromChats | Информация по номерам телефонов из целевых чатов | нет | | | | cpa | CpaChat | get_phones_info_from_chats | GetPhonesInfoFromChatsRequest | GetPhonesInfoFromChatsResponse | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v2/balanceInfo | Баланс (deprecated) | да | 1.1.0 | cpa_lead().get_balance_info | 1.3.0 | cpa | CpaArchive | get_balance_info | LegacyCreateBalanceInfoV2Request | LegacyCreateBalanceInfoV2Response | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v2/callById | Звонок | да | 1.1.0 | call_tracking_call().get | 1.3.0 | cpa | CpaArchive | get_call_by_id | LegacyCreateCallByIdV2Request | LegacyCreateCallByIdV2Response | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v2/callsByTime | Звонки по времени | нет | | | | cpa | CpaCall | create_calls_by_time_v2 | CreateCallsByTimeV2Request | CreateCallsByTimeV2Response | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v2/chatsByTime | Чаты по времени | нет | | | | cpa | CpaChat | create_chats_by_time | CreateChatsByTimeRequest | CreateChatsByTimeResponse | контракт+маппинг | | +| cpa | CPAАвито.json | POST | /cpa/v3/balanceInfo | Баланс | нет | | | | cpa | CpaLead | create_balance_info_v3 | CreateBalanceInfoV3Request | CreateBalanceInfoV3Response | контракт+маппинг | | +| cpa | CallTracking[КТ].json | POST | /calltracking/v1/getCallById/ | Звонок по идентификатору | нет | | | | cpa | CallTrackingCall | create_call_by_id | CreateCallByIdRequest | EmptyResponse | контракт+маппинг | | +| cpa | CallTracking[КТ].json | POST | /calltracking/v1/getCalls/ | Звонки по времени | нет | | | | cpa | CallTrackingCall | create_calls | CreateCallsRequest | EmptyResponse | контракт+маппинг | | +| cpa | CallTracking[КТ].json | GET | /calltracking/v1/getRecordByCallId/ | Получение аудиозаписи звонка по идентификатору | нет | | | | cpa | CallTrackingCall | get_record_by_call_id | NoRequest | EmptyResponse | контракт+маппинг | | +| promotion | TrxPromo.json | POST | /trx-promo/1/apply | Запуск продвижения | нет | | | | promotion | TrxPromotion | create_trx_promo_open_api_apply | CreateTrxPromoOpenApiApplyRequest | CreateTrxPromoOpenApiApplyResponse | контракт+маппинг | | +| promotion | TrxPromo.json | POST | /trx-promo/1/cancel | Остановка продвижения | нет | | | | promotion | TrxPromotion | delete_trx_promo_open_api_cancel | DeleteTrxPromoOpenApiCancelRequest | DeleteTrxPromoOpenApiCancelResponse | контракт+маппинг | | +| promotion | TrxPromo.json | GET | /trx-promo/1/commissions | Проверка доступности продвижения и размера комиссий | нет | | | | promotion | TrxPromotion | get_trx_promo_open_api_commissions | GetTrxPromoOpenApiCommissionsRequest | GetTrxPromoOpenApiCommissionsResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v1/applications/apply_actions | Батчевая смена статуса откликов | нет | | | | jobs | Application | get_applications_apply_actions | GetApplicationsApplyActionsRequest | GetApplicationsApplyActionsResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v1/applications/get_by_ids | Получение списка откликов | нет | | | | jobs | Application | list_applications_get_by_ids | ListApplicationsGetByIdsRequest | ListApplicationsGetByIdsResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v1/applications/get_ids | Получение идентификаторов откликов | нет | | | | jobs | Application | list_applications_get_ids | NoRequest | ListApplicationsGetIdsResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v1/applications/get_states | Получение списка возможных статусов откликов | нет | | | | jobs | Application | list_applications_get_states | NoRequest | ListApplicationsGetStatesResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v1/applications/set_is_viewed | Изменение статуса отклика | нет | | | | jobs | Application | get_applications_set_is_viewed | GetApplicationsSetIsViewedRequest | GetApplicationsSetIsViewedResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | DELETE | /job/v1/applications/webhook | Отключение уведомлений по откликам (webhook) | нет | | | | jobs | JobWebhook | delete_applications_webhook_delete | NoRequest | DeleteApplicationsWebhookDeleteResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v1/applications/webhook | Получение информации о подписках (webhook) | нет | | | | jobs | JobWebhook | get_applications_webhook_get | NoRequest | GetApplicationsWebhookGetResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | PUT | /job/v1/applications/webhook | Включение уведомлений по откликам (webhook) | нет | | | | jobs | JobWebhook | update_applications_webhook_put | UpdateApplicationsWebhookPutRequest | UpdateApplicationsWebhookPutResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v1/applications/webhooks | Получение списка подписок (webhook) | нет | | | | jobs | JobWebhook | list_applications_webhooks_get | NoRequest | ListApplicationsWebhooksGetResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v1/resumes/ | Поиск резюме | нет | | | | jobs | Resume | list_resumes_get | NoRequest | ListResumesGetResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v1/resumes/{resume_id}/contacts/ | Доступ к контактным данным соискателя | нет | | | | jobs | Resume | get_resume_get_contacts | NoRequest | GetResumeGetContactsResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v1/vacancies | Публикация вакансии | нет | | | | jobs | Vacancy | create_vacancy_create | CreateVacancyCreateRequest | CreateVacancyCreateResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | PUT | /job/v1/vacancies/archived/{vacancy_id} | Остановка публикации вакансии | нет | | | | jobs | Vacancy | delete_vacancy_archive | DeleteVacancyArchiveRequest | EmptyResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | PUT | /job/v1/vacancies/{vacancy_id} | Редактирование вакансии | нет | | | | jobs | Vacancy | update_vacancy_update | UpdateVacancyUpdateRequest | EmptyResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v1/vacancies/{vacancy_id}/prolongate | Реактивация вакансии | нет | | | | jobs | Vacancy | create_vacancy_prolongate | CreateVacancyProlongateRequest | EmptyResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v2/resumes/{resume_id} | Просмотр данных резюме | нет | | | | jobs | Resume | get_resume_get_item | NoRequest | GetResumeGetItemResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v2/vacancies | Поиск вакансий | нет | | | | jobs | Vacancy | list_search_vacancy | NoRequest | ListSearchVacancyResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v2/vacancies | Публикация вакансии v2 | нет | | | | jobs | Vacancy | create_vacancy_create_v2 | CreateVacancyCreateV2Request | CreateVacancyCreateV2Response | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v2/vacancies/batch | Просмотр данных вакансий | нет | | | | jobs | Vacancy | get_vacancies_get_by_ids | GetVacanciesGetByIdsRequest | GetVacanciesGetByIdsResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v2/vacancies/statuses | Получение статуса публикации вакансий V2 | нет | | | | jobs | Vacancy | get_vacancy_get_statuses | GetVacancyGetStatusesRequest | GetVacancyGetStatusesResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | POST | /job/v2/vacancies/update/{vacancy_uuid} | Редактирование вакансии v2 | нет | | | | jobs | Vacancy | update_vacancy_update_v2 | UpdateVacancyUpdateV2Request | UpdateVacancyUpdateV2Response | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v2/vacancies/{vacancy_id} | Просмотр данных вакансии | нет | | | | jobs | Vacancy | get_vacancy_get_item | NoRequest | GetVacancyGetItemResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | PUT | /job/v2/vacancies/{vacancy_uuid}/auto_renewal | Автопродление вакансии v2 | нет | | | | jobs | Vacancy | update_vacancy_auto_renewal | UpdateVacancyAutoRenewalRequest | EmptyResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v2/vacancy/dict | Получение списка доступных словарей | нет | | | | jobs | JobDictionary | list_dicts | NoRequest | ListDictsResponse | контракт+маппинг | | +| jobs | АвитоРабота.json | GET | /job/v2/vacancy/dict/{dictionary_id} | Получение доступных значений списка по ID словаря | нет | | | | jobs | JobDictionary | list_dict_by_id | NoRequest | ListDictByIdResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v1/profile | Получение профиля пользователя автозагрузки (deprecated) | да | 1.1.0 | autoload_profile().get | 1.3.0 | ads | AutoloadArchive | get_profile | NoRequest | LegacyGetProfileResponse | контракт+маппинг | | +| ads | Автозагрузка.json | POST | /autoload/v1/profile | Создание/редактирование настроек профиля пользователя автозагрузки (deprecated) | да | 1.1.0 | autoload_profile().save | 1.3.0 | ads | AutoloadArchive | save_profile | LegacyCreateOrUpdateProfileRequest | EmptyResponse | контракт+маппинг | | +| ads | Автозагрузка.json | POST | /autoload/v1/upload | Загрузка файла по ссылке | нет | | | | ads | AutoloadProfile | create_upload | NoRequest | EmptyResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v1/user-docs/node/{node_slug}/fields | Получения полей категории | нет | | | | ads | AutoloadProfile | get_user_docs_node_fields | NoRequest | GetUserDocsNodeFieldsResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v1/user-docs/tree | Получение дерева категорий | нет | | | | ads | AutoloadProfile | get_user_docs_tree | NoRequest | GetUserDocsTreeResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/items/ad_ids | ID объявлений из файла | нет | | | | ads | AutoloadReport | get_ad_ids_by_avito_ids | NoRequest | GetAdIdsByAvitoIdsResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/items/avito_ids | ID объявлений на Авито | нет | | | | ads | AutoloadReport | get_avito_ids_by_ad_ids | NoRequest | GetAvitoIdsByAdIdsResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/profile | Получение профиля пользователя автозагрузки | нет | | | | ads | AutoloadProfile | get_profile_v2 | NoRequest | GetProfileV2Response | контракт+маппинг | | +| ads | Автозагрузка.json | POST | /autoload/v2/profile | Создание/редактирование настроек профиля пользователя автозагрузки | нет | | | | ads | AutoloadProfile | create_or_update_profile_v2 | CreateOrUpdateProfileV2Request | EmptyResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/reports | Список отчётов автозагрузки | нет | | | | ads | AutoloadReport | list_reports_v2 | NoRequest | ListReportsV2Response | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/reports/items | Объявления по ID в автозагрузке | нет | | | | ads | AutoloadReport | get_autoload_items_info_v2 | NoRequest | GetAutoloadItemsInfoV2Response | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/reports/last_completed_report | Статистика по последней выгрузке (deprecated) | да | 1.1.0 | autoload_report().get_last_completed | 1.3.0 | ads | AutoloadArchive | get_last_completed_report | NoRequest | LegacyGetLastCompletedReportResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/reports/{report_id} | Статистика по конкретной выгрузке (deprecated) | да | 1.1.0 | autoload_report().get | 1.3.0 | ads | AutoloadArchive | get_report | NoRequest | LegacyGetReportByIdV2Response | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/reports/{report_id}/items | Все объявления из конкретной выгрузки | нет | | | | ads | AutoloadReport | get_report_items_by_id | NoRequest | GetReportItemsByIdResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v2/reports/{report_id}/items/fees | Списания за объявления в конкретной выгрузке | нет | | | | ads | AutoloadReport | get_report_items_fees_by_id | NoRequest | GetReportItemsFeesByIdResponse | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v3/reports/last_completed_report | Статистика по последней выгрузке | нет | | | | ads | AutoloadReport | get_last_completed_report_v3 | NoRequest | GetLastCompletedReportV3Response | контракт+маппинг | | +| ads | Автозагрузка.json | GET | /autoload/v3/reports/{report_id} | Статистика по конкретной выгрузке | нет | | | | ads | AutoloadReport | get_report_by_id_v3 | NoRequest | GetReportByIdV3Response | контракт+маппинг | | +| auth | Авторизация.json | POST | /token | Получение access token | нет | | | | auth | AvitoClient.auth() | get_access_token | GetAccessTokenRequest | GetAccessTokenResponse | контракт+маппинг | канонический token-endpoint | +| auth | Авторизация.json | POST | /token | Получение access token | нет | | | | auth | AvitoClient.auth() | get_access_token_authorization_code | GetAccessTokenAuthorizationCodeRequest | GetAccessTokenAuthorizationCodeResponse | контракт+маппинг | нормализованы скрытые Unicode-символы в пути /token для inventory | +| auth | Авторизация.json | POST | /token | Обновление access token | нет | | | | auth | AvitoClient.auth() | update_refresh_access_token_authorization_code | UpdateRefreshAccessTokenAuthorizationCodeRequest | UpdateRefreshAccessTokenAuthorizationCodeResponse | контракт+маппинг | нормализованы скрытые Unicode-символы в пути /token для inventory | +| promotion | Автостратегия.json | POST | /autostrategy/v1/budget | Расчет бюджета кампании | нет | | | | promotion | AutostrategyCampaign | create_autostrategy_budget | CreateAutostrategyBudgetRequest | CreateAutostrategyBudgetResponse | контракт+маппинг | | +| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/create | Создание новой кампании | нет | | | | promotion | AutostrategyCampaign | create_autostrategy_campaign | CreateAutostrategyCampaignRequest | CreateAutostrategyCampaignResponse | контракт+маппинг | | +| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/edit | Редактирование кампании | нет | | | | promotion | AutostrategyCampaign | update_edit_autostrategy_campaign | UpdateEditAutostrategyCampaignRequest | UpdateEditAutostrategyCampaignResponse | контракт+маппинг | | +| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/info | Получение полной информации о кампании | нет | | | | promotion | AutostrategyCampaign | get_autostrategy_campaign_info | GetAutostrategyCampaignInfoRequest | GetAutostrategyCampaignInfoResponse | контракт+маппинг | | +| promotion | Автостратегия.json | POST | /autostrategy/v1/campaign/stop | Остановка кампании | нет | | | | promotion | AutostrategyCampaign | delete_stop_autostrategy_campaign | DeleteStopAutostrategyCampaignRequest | DeleteStopAutostrategyCampaignResponse | контракт+маппинг | | +| promotion | Автостратегия.json | POST | /autostrategy/v1/campaigns | Получение списка кампаний | нет | | | | promotion | AutostrategyCampaign | list_autostrategy_campaigns | ListAutostrategyCampaignsRequest | ListAutostrategyCampaignsResponse | контракт+маппинг | | +| promotion | Автостратегия.json | POST | /autostrategy/v1/stat | Получение статистики по кампании | нет | | | | promotion | AutostrategyCampaign | get_autostrategy_stat | GetAutostrategyStatRequest | GetAutostrategyStatResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/catalogs/resolve | Получение актуальных параметров Автокаталога | нет | | | | autoteka | AutotekaVehicle | get_catalogs_resolve | GetCatalogsResolveRequest | GetCatalogsResolveResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/get-leads/ | Получение событий сервиса Сигнал | нет | | | | autoteka | AutotekaVehicle | get_leads | GetLeadsRequest | GetLeadsResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/monitoring/bucket/add | Добавить идентификаторы (vin/frame) на мониторинг | нет | | | | autoteka | AutotekaMonitoring | create_monitoring_bucket_add | CreateMonitoringBucketAddRequest | CreateMonitoringBucketAddResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/monitoring/bucket/delete | Полная очистка списка мониторинга | нет | | | | autoteka | AutotekaMonitoring | list_monitoring_bucket_delete | NoRequest | ListMonitoringBucketDeleteResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/monitoring/bucket/remove | Удаление идентификаторов из мониторинга (vin/frame) | нет | | | | autoteka | AutotekaMonitoring | delete_monitoring_bucket_remove | DeleteMonitoringBucketRemoveRequest | DeleteMonitoringBucketRemoveResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/monitoring/get-reg-actions/ | Получение событий мониторинга | нет | | | | autoteka | AutotekaMonitoring | get_monitoring_get_reg_actions | NoRequest | GetMonitoringGetRegActionsResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/packages/active_package | Запрос остатка отчётов пользователя | нет | | | | autoteka | AutotekaReport | get_active_package | NoRequest | GetActivePackageResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/previews | Превью по VIN или номеру кузова | нет | | | | autoteka | AutotekaVehicle | create_preview_by_vin | CreatePreviewByVinRequest | CreatePreviewByVinResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/previews/{previewId} | Получение превью по его ID | нет | | | | autoteka | AutotekaVehicle | get_preview | NoRequest | GetPreviewResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/reports | Отчет по превью | нет | | | | autoteka | AutotekaReport | create_report | CreateReportRequest | CreateReportResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/reports-by-vehicle-id | Отчет по идентификатору авто (vin/frame) | нет | | | | autoteka | AutotekaReport | create_report_by_vehicle_id | CreateReportByVehicleIdRequest | CreateReportByVehicleIdResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/reports/list/ | Получение списка отчётов | нет | | | | autoteka | AutotekaReport | list_report_list | NoRequest | ListReportListResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/reports/{report_id} | Получение отчета по его ID | нет | | | | autoteka | AutotekaReport | get_report | NoRequest | GetReportResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/request-preview-by-external-item | Превью по ID объявления другой площадки | нет | | | | autoteka | AutotekaVehicle | create_preview_by_external_item | CreatePreviewByExternalItemRequest | CreatePreviewByExternalItemResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/request-preview-by-item-id | Превью по ID объявления Авито | нет | | | | autoteka | AutotekaVehicle | create_preview_by_item_id | CreatePreviewByItemIdRequest | CreatePreviewByItemIdResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/request-preview-by-regnumber | Превью по государственному номеру | нет | | | | autoteka | AutotekaVehicle | create_preview_by_reg_number | CreatePreviewByRegNumberRequest | CreatePreviewByRegNumberResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/scoring/by-vehicle-id | Скоринг рисков по идентификатору авто (vin/frame) | нет | | | | autoteka | AutotekaScoring | create_scoring_by_vehicle_id | CreateScoringByVehicleIdRequest | CreateScoringByVehicleIdResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/scoring/{scoring_id} | Получение скоринга рисков по его ID | нет | | | | autoteka | AutotekaScoring | get_scoring_get_by_id | NoRequest | GetScoringGetByIdResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/specifications/by-plate-number | Запрос характеристик по регистрационному номеру | нет | | | | autoteka | AutotekaVehicle | create_specification_by_plate_number | CreateSpecificationByPlateNumberRequest | CreateSpecificationByPlateNumberResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/specifications/by-vehicle-id | Запрос характеристик по идентификатору авто (vin/frame) | нет | | | | autoteka | AutotekaVehicle | create_specification_by_vehicle_id | CreateSpecificationByVehicleIdRequest | CreateSpecificationByVehicleIdResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/specifications/specification/{specificationID} | Получение характеристик по ID запроса | нет | | | | autoteka | AutotekaVehicle | get_specification_get_by_id | NoRequest | GetSpecificationGetByIdResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/sync/create-by-regnumber | Синхронное создание отчета по ГРЗ | нет | | | | autoteka | AutotekaReport | create_sync_create_report_by_reg_number | CreateSyncCreateReportByRegNumberRequest | CreateSyncCreateReportByRegNumberResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/sync/create-by-vin | Синхронное создание отчёта по VIN или номеру кузова | нет | | | | autoteka | AutotekaReport | create_sync_create_report_by_vin | CreateSyncCreateReportByVinRequest | CreateSyncCreateReportByVinResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/teasers | Тизер по идентификатору авто (vin/frame) | нет | | | | autoteka | AutotekaVehicle | create_teaser | CreateTeaserRequest | CreateTeaserResponse | контракт+маппинг | | +| autoteka | Автотека.json | GET | /autoteka/v1/teasers/{teaser_id} | Получение тизера по ID тизера | нет | | | | autoteka | AutotekaVehicle | get_teaser | NoRequest | GetTeaserResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /autoteka/v1/valuation/by-specification | Получение оценки по параметрам | нет | | | | autoteka | AutotekaValuation | get_valuation_by_specification | GetValuationBySpecificationRequest | GetValuationBySpecificationResponse | контракт+маппинг | | +| autoteka | Автотека.json | POST | /token | Получение access token | нет | | | | autoteka | AvitoClient.auth() | get_access_token | NoRequest | GetAccessTokenResponse | контракт+маппинг | | +| realty | Аналитикапонедвижимости.json | GET | /realty/v1/marketPriceCorrespondence/{itemId}/{price} | Получение соответствия переданной цены рыночной цене | нет | | | | realty | RealtyAnalyticsReport | get_market_price_correspondence_v1 | NoRequest | GetMarketPriceCorrespondenceV1Response | контракт+маппинг | | +| realty | Аналитикапонедвижимости.json | POST | /realty/v1/report/create/{itemId} | Получение аналитического отчета по недвижимости | нет | | | | realty | RealtyAnalyticsReport | get_report_for_classified | NoRequest | GetReportForClassifiedResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /cancelAnnouncement | Отмена анонса в СД | нет | | | | orders | DeliveryOrder | delete_cancel_announcement3_pl | DeleteCancelAnnouncement3PlRequest | DeleteCancelAnnouncement3PlResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /createAnnouncement | Создание анонса в СД | нет | | | | orders | DeliveryOrder | create_announcement3_pl | CreateAnnouncement3PlRequest | CreateAnnouncement3PlResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /createParcel | Создание посылки | нет | | | | orders | DeliveryOrder | create_parcel | CreateParcelRequest | CreateParcelResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/announcements/create | Создание анонса в Avito | нет | | | | orders | SandboxDelivery | create_announcement | CreateAnnouncementRequest | CreateAnnouncementResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/announcements/track | Трекинг анонсов | нет | | | | orders | SandboxDelivery | create_track_announcement | CreateTrackAnnouncementRequest | CreateTrackAnnouncementResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/areas/custom-schedule | Установка графика работы на определённый день | нет | | | | orders | SandboxDelivery | update_custom_area_schedule | UpdateCustomAreaScheduleRequest | UpdateCustomAreaScheduleResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/cancelParcel | Отмена посылки | нет | | | | orders | SandboxDelivery | delete_cancel_parcel | DeleteCancelParcelRequest | DeleteCancelParcelResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/order/checkConfirmationCode | Проверка кода подтверждения | нет | | | | orders | SandboxDelivery | get_check_confirmation_code | GetCheckConfirmationCodeRequest | GetCheckConfirmationCodeResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/order/properties | Добавление / изменение параметров доставки посылки | нет | | | | orders | SandboxDelivery | create_set_order_properties | CreateSetOrderPropertiesRequest | CreateSetOrderPropertiesResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/order/realAddress | Фактический адрес приёма / возврата посылки | нет | | | | orders | SandboxDelivery | create_set_order_real_address | CreateSetOrderRealAddressRequest | CreateSetOrderRealAddressResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/order/tracking | Трекинг | нет | | | | orders | SandboxDelivery | create_tracking | CreateTrackingRequest | CreateTrackingResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/prohibitOrderAcceptance | Запрет приёма посылки от отправителя | нет | | | | orders | SandboxDelivery | delete_prohibit_order_acceptance | DeleteProhibitOrderAcceptanceRequest | DeleteProhibitOrderAcceptanceResponse | контракт+маппинг | | +| orders | Доставка.json | GET | /delivery-sandbox/sorting-center | Получить список сортировочных центров | нет | | | | orders | SandboxDelivery | list_sorting_center | NoRequest | ListSortingCenterResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/tariffs/sorting-center | Загрузить сортировочные центры | нет | | | | orders | SandboxDelivery | create_add_sorting_center | CreateAddSortingCenterRequest | CreateAddSortingCenterResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/areas | Загрузить области доставки | нет | | | | orders | SandboxDelivery | create_add_areas_sandbox | CreateAddAreasSandboxRequest | CreateAddAreasSandboxResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/tagged-sorting-centers | Установка тэгов своим и/или чужим сортировочным центрам | нет | | | | orders | SandboxDelivery | update_add_tags_to_sorting_center | UpdateAddTagsToSortingCenterRequest | UpdateAddTagsToSortingCenterResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/terminals | Загрузить терминалы | нет | | | | orders | SandboxDelivery | create_add_terminals_sandbox | CreateAddTerminalsSandboxRequest | CreateAddTerminalsSandboxResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/tariffs/{tariff_id}/terms | Обновить сроки по тарифу | нет | | | | orders | SandboxDelivery | update_update_terms | UpdateUpdateTermsRequest | UpdateUpdateTermsResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/tariffsV2 | Загрузить новый тариф v2 | нет | | | | orders | SandboxDelivery | create_add_tariff_sandbox_v2 | CreateAddTariffSandboxV2Request | CreateAddTariffSandboxV2Response | контракт+маппинг | | +| orders | Доставка.json | GET | /delivery-sandbox/tasks/{task_id} | Получение информации по задаче | нет | | | | orders | DeliveryTask | get_task | NoRequest | GetTaskResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/cancelAnnouncement | Отправка события об отмене тестового анонса | нет | | | | orders | SandboxDelivery | create_v1cancel_announcement | CreateV1cancelAnnouncementRequest | CreateV1cancelAnnouncementResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/cancelParcel | Отмена тестовой посылки | нет | | | | orders | SandboxDelivery | delete_v1_cancel_parcel | DeleteV1CancelParcelRequest | DeleteV1CancelParcelResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/changeParcel | Создание заявки на изменение данных тестовой посылки | нет | | | | orders | SandboxDelivery | create_v1change_parcel | CreateV1changeParcelRequest | CreateV1changeParcelResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/createAnnouncement | Создание тестового анонса | нет | | | | orders | SandboxDelivery | create_v1create_announcement | CreateV1createAnnouncementRequest | CreateV1createAnnouncementResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/getAnnouncementEvent | Получение последнего события тестового анонса | нет | | | | orders | SandboxDelivery | get_v1get_announcement_event | GetV1getAnnouncementEventRequest | GetV1getAnnouncementEventResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/getChangeParcelInfo | Получение информации об изменении тестовой посылки | нет | | | | orders | SandboxDelivery | get_v1get_change_parcel_info | GetV1getChangeParcelInfoRequest | GetV1getChangeParcelInfoResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/getParcelInfo | Получение информации о тестовой посылке | нет | | | | orders | SandboxDelivery | get_v1get_parcel_info | GetV1getParcelInfoRequest | GetV1getParcelInfoResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v1/getRegisteredParcelID | Получение ID зарегистрированной тестовой посылки | нет | | | | orders | SandboxDelivery | get_v1get_registered_parcel_id | GetV1getRegisteredParcelIdRequest | GetV1getRegisteredParcelIdResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery-sandbox/v2/createParcel | Создание тестовой посылки | нет | | | | orders | SandboxDelivery | create_sandbox_parcel_v2 | CreateSandboxParcelV2Request | CreateSandboxParcelV2Response | контракт+маппинг | | +| orders | Доставка.json | POST | /delivery/order/changeParcelResult | Отправка результата исполнения заявки | нет | | | | orders | DeliveryOrder | create_change_parcel_result | CreateChangeParcelResultRequest | CreateChangeParcelResultResponse | контракт+маппинг | | +| orders | Доставка.json | POST | /sandbox/changeParcels | Обновление свойств посылок | нет | | | | orders | DeliveryOrder | update_change_parcels | UpdateChangeParcelsRequest | UpdateChangeParcelsResponse | контракт+маппинг | | +| accounts | ИерархияАккаунтов.json | GET | /checkAhUserV1 | Получение информации о статусе пользователя в ИА | нет | | | | accounts | AccountHierarchy | get_check_ah_user_v1 | NoRequest | GetCheckAhUserV1Response | контракт+маппинг | | +| accounts | ИерархияАккаунтов.json | GET | /getEmployeesV1 | Получение списка сотрудников иерархии | нет | | | | accounts | AccountHierarchy | list_employees_v1 | NoRequest | ListEmployeesV1Response | контракт+маппинг | | +| accounts | ИерархияАккаунтов.json | POST | /linkItemsV1 | Прикрепление сотрудника иерархии к объявлениям, перезакрепление объявлений между сотрудниками иерархии | нет | | | | accounts | AccountHierarchy | create_link_items_v1 | CreateLinkItemsV1Request | EmptyResponse | контракт+маппинг | | +| accounts | ИерархияАккаунтов.json | GET | /listCompanyPhonesV1 | Получение списка телефонов компании | нет | | | | accounts | AccountHierarchy | list_company_phones_v1 | NoRequest | ListCompanyPhonesV1Response | контракт+маппинг | | +| accounts | ИерархияАккаунтов.json | POST | /listItemsByEmployeeIdV1 | Получение списка объявлений по сотруднику | нет | | | | accounts | AccountHierarchy | list_items_by_employee_id_v1 | ListItemsByEmployeeIdV1Request | ListItemsByEmployeeIdV1Response | контракт+маппинг | | +| accounts | Информацияопользователе.json | POST | /core/v1/accounts/operations_history/ | Получение истории операций пользователя | нет | | | | accounts | Account | get_operations_history | GetOperationsHistoryRequest | GetOperationsHistoryResponse | контракт+маппинг | | +| accounts | Информацияопользователе.json | GET | /core/v1/accounts/self | Получение информации об авторизованном пользователе | нет | | | | accounts | Account | get_user_info_self | NoRequest | GetUserInfoSelfResponse | контракт+маппинг | | +| accounts | Информацияопользователе.json | GET | /core/v1/accounts/{user_id}/balance/ | Получение баланса кошелька пользователя | нет | | | | accounts | Account | get_user_balance | NoRequest | GetUserBalanceResponse | контракт+маппинг | | +| realty | Краткосрочнаяаренда.json | POST | /core/v1/accounts/{user_id}/items/{item_id}/bookings | Заполнение календаря занятости объекта недвижимости | нет | | | | realty | RealtyBooking | update_bookings_info | UpdateBookingsInfoRequest | UpdateBookingsInfoResponse | контракт+маппинг | | +| realty | Краткосрочнаяаренда.json | GET | /realty/v1/accounts/{user_id}/items/{item_id}/bookings | Получение списка броней по объявлению | нет | | | | realty | RealtyBooking | list_realty_bookings | NoRequest | ListRealtyBookingsResponse | контракт+маппинг | | +| realty | Краткосрочнаяаренда.json | POST | /realty/v1/accounts/{user_id}/items/{item_id}/prices | Актуализация параметров для выбранных периодов | нет | | | | realty | RealtyPricing | update_realty_prices | UpdateRealtyPricesRequest | UpdateRealtyPricesResponse | контракт+маппинг | | +| realty | Краткосрочнаяаренда.json | POST | /realty/v1/items/intervals | Заполнение доступности объекта недвижимости с квотами и без | нет | | | | realty | RealtyListing | get_intervals | GetIntervalsRequest | GetIntervalsResponse | контракт+маппинг | | +| realty | Краткосрочнаяаренда.json | POST | /realty/v1/items/{item_id}/base | Установка базовых параметров | нет | | | | realty | RealtyListing | update_base_params | UpdateBaseParamsRequest | EmptyResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/messages | Отправка сообщения | нет | | | | messenger | ChatMessage | create_send_message | CreateSendMessageRequest | CreateSendMessageResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/messages/image | Отправка сообщения с изображением | нет | | | | messenger | ChatMessage | create_send_image_message | CreateSendImageMessageRequest | CreateSendImageMessageResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/messages/{message_id} | Удаление сообщения | нет | | | | messenger | ChatMessage | delete_message | NoRequest | DeleteMessageResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/chats/{chat_id}/read | Прочитать чат | нет | | | | messenger | Chat | create_chat_read | NoRequest | CreateChatReadResponse | контракт+маппинг | | +| messenger | Мессенджер.json | GET | /messenger/v1/accounts/{user_id}/getVoiceFiles | Получение голосовых сообщений | нет | | | | messenger | ChatMedia | get_voice_files | NoRequest | GetVoiceFilesResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v1/accounts/{user_id}/uploadImages | Загрузка изображений | нет | | | | messenger | ChatMedia | create_upload_images | CreateUploadImagesMultipartRequest | CreateUploadImagesResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v1/subscriptions | Получение подписок (webhooks) | нет | | | | messenger | ChatWebhook | get_subscriptions | NoRequest | GetSubscriptionsResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v1/webhook/unsubscribe | Отключение уведомлений (webhooks) | нет | | | | messenger | ChatWebhook | delete_webhook_unsubscribe | DeleteWebhookUnsubscribeRequest | DeleteWebhookUnsubscribeResponse | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v2/accounts/{user_id}/blacklist | Добавление пользователя в blacklist | нет | | | | messenger | Chat | create_blacklist_v2 | CreateBlacklistV2Request | EmptyResponse | контракт+маппинг | | +| messenger | Мессенджер.json | GET | /messenger/v2/accounts/{user_id}/chats | Получение информации по чатам | нет | | | | messenger | Chat | get_chats_v2 | NoRequest | GetChatsV2Response | контракт+маппинг | | +| messenger | Мессенджер.json | GET | /messenger/v2/accounts/{user_id}/chats/{chat_id} | Получение информации по чату | нет | | | | messenger | Chat | get_chat_by_id_v2 | NoRequest | GetChatByIdV2Response | контракт+маппинг | | +| messenger | Мессенджер.json | GET | /messenger/v3/accounts/{user_id}/chats/{chat_id}/messages/ | Получение списка сообщений V3 | нет | | | | messenger | ChatMessage | list_messages_v3 | NoRequest | ListMessagesV3Response | контракт+маппинг | | +| messenger | Мессенджер.json | POST | /messenger/v3/webhook | Включение уведомлений V3 (webhooks) | нет | | | | messenger | ChatWebhook | update_webhook_v3 | UpdateWebhookV3Request | UpdateWebhookV3Response | контракт+маппинг | | +| promotion | Настройкаценыцелевогодействия.json | GET | /cpxpromo/1/getBids/{itemId} | Получение детализированной информации о действующих и доступных ценах за целевые действия и бюджетах | нет | | | | promotion | TargetActionPricing | get_bids | NoRequest | GetBidsResponse | контракт+маппинг | | +| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/getPromotionsByItemIds | Получение текущих цен за целевое действие и бюджетов по нескольким объявлениям | нет | | | | promotion | TargetActionPricing | get_promotions_by_item_ids | GetPromotionsByItemIdsRequest | GetPromotionsByItemIdsResponse | контракт+маппинг | | +| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/remove | Остановка продвижения | нет | | | | promotion | TargetActionPricing | delete_promotion | DeletePromotionRequest | DeletePromotionResponse | контракт+маппинг | | +| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/setAuto | Применение автоматической настройки | нет | | | | promotion | TargetActionPricing | update_auto_bid | UpdateAutoBidRequest | EmptyResponse | контракт+маппинг | | +| promotion | Настройкаценыцелевогодействия.json | POST | /cpxpromo/1/setManual | Применение ручной настройки | нет | | | | promotion | TargetActionPricing | update_manual_bid | UpdateManualBidRequest | EmptyResponse | контракт+маппинг | | +| ads | Объявления.json | POST | /core/v1/accounts/{userId}/vas/prices | Получение информации о стоимости услуг продвижения и доступных значках | нет | | | | ads | AdPromotion | get_vas_prices | GetVasPricesRequest | GetVasPricesResponse | контракт+маппинг | | +| ads | Объявления.json | POST | /core/v1/accounts/{user_id}/calls/stats/ | Получение статистики по звонкам | нет | | | | ads | AdStats | get_calls_stats | GetCallsStatsRequest | GetCallsStatsResponse | контракт+маппинг | | +| ads | Объявления.json | GET | /core/v1/accounts/{user_id}/items/{item_id}/ | Получение информации по объявлению | нет | | | | ads | Ad | get_item_info | NoRequest | GetItemInfoResponse | контракт+маппинг | | +| ads | Объявления.json | PUT | /core/v1/accounts/{user_id}/items/{item_id}/vas | Применение дополнительных услуг | нет | | | | ads | AdPromotion | update_item_vas | UpdateItemVasRequest | UpdateItemVasResponse | контракт+маппинг | | +| ads | Объявления.json | GET | /core/v1/items | Получение информации по объявлениям | нет | | | | ads | Ad | get_items_info | NoRequest | GetItemsInfoResponse | контракт+маппинг | | +| ads | Объявления.json | POST | /core/v1/items/{item_id}/update_price | Обновление цены объявления | нет | | | | ads | Ad | update_update_price | UpdateUpdatePriceRequest | UpdateUpdatePriceResponse | контракт+маппинг | | +| ads | Объявления.json | PUT | /core/v2/accounts/{user_id}/items/{item_id}/vas_packages | Применение пакета дополнительных услуг | нет | | | | ads | AdPromotion | update_item_vas_package_v2 | UpdateItemVasPackageV2Request | UpdateItemVasPackageV2Response | контракт+маппинг | | +| ads | Объявления.json | PUT | /core/v2/items/{itemId}/vas/ | Применение услуг продвижения | нет | | | | ads | AdPromotion | update_apply_vas | UpdateApplyVasRequest | UpdateApplyVasResponse | контракт+маппинг | | +| ads | Объявления.json | POST | /stats/v1/accounts/{user_id}/items | Получение статистики по списку объявлений | нет | | | | ads | AdStats | get_item_stats_shallow | GetItemStatsShallowRequest | GetItemStatsShallowResponse | контракт+маппинг | | +| ads | Объявления.json | POST | /stats/v2/accounts/{user_id}/items | Получение статистических показателей по профилю | нет | | | | ads | AdStats | get_item_analytics | GetItemAnalyticsRequest | GetItemAnalyticsResponse | контракт+маппинг | | +| ads | Объявления.json | POST | /stats/v2/accounts/{user_id}/spendings | Получение статистики расходов профиля | нет | | | | ads | AdStats | get_account_spendings | GetAccountSpendingsRequest | GetAccountSpendingsResponse | контракт+маппинг | | +| promotion | Продвижение.json | POST | /promotion/v1/items/services/bbip/forecasts/get | BBIP. Прогноз продвижения | нет | | | | promotion | BbipPromotion | create_bbip_forecasts_by_items_v1 | CreateBbipForecastsByItemsV1Request | CreateBbipForecastsByItemsV1Response | контракт+маппинг | | +| promotion | Продвижение.json | PUT | /promotion/v1/items/services/bbip/orders/create | BBIP. Подключение услуги продвижения | нет | | | | promotion | BbipPromotion | update_bbip_order_for_items_v1 | UpdateBbipOrderForItemsV1Request | UpdateBbipOrderForItemsV1Response | контракт+маппинг | | +| promotion | Продвижение.json | POST | /promotion/v1/items/services/bbip/suggests/get | BBIP. Варианты бюджета продвижения | нет | | | | promotion | BbipPromotion | create_bbip_suggests_by_items_v1 | CreateBbipSuggestsByItemsV1Request | CreateBbipSuggestsByItemsV1Response | контракт+маппинг | | +| promotion | Продвижение.json | POST | /promotion/v1/items/services/dict | Словарь типов услуг продвижения | нет | | | | promotion | PromotionOrder | create_dict_of_services_v1 | NoRequest | CreateDictOfServicesV1Response | контракт+маппинг | | +| promotion | Продвижение.json | POST | /promotion/v1/items/services/get | Список услуг продвижения | нет | | | | promotion | PromotionOrder | list_services_by_items_v1 | ListServicesByItemsV1Request | ListServicesByItemsV1Response | контракт+маппинг | | +| promotion | Продвижение.json | POST | /promotion/v1/items/services/orders/get | Список заявок | нет | | | | promotion | PromotionOrder | list_orders_by_user_v1 | ListOrdersByUserV1Request | ListOrdersByUserV1Response | контракт+маппинг | | +| promotion | Продвижение.json | POST | /promotion/v1/items/services/orders/status | Статус заявки | нет | | | | promotion | PromotionOrder | get_order_status_v1 | GetOrderStatusV1Request | GetOrderStatusV1Response | контракт+маппинг | | +| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/available | Получение информации об объявлениях | нет | | | | messenger | SpecialOfferCampaign | get_available | GetAvailableRequest | GetAvailableResponse | контракт+маппинг | | +| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/multiConfirm | Отправка и оплата рассылки | нет | | | | messenger | SpecialOfferCampaign | create_multi_confirm | CreateMultiConfirmRequest | CreateMultiConfirmResponse | контракт+маппинг | | +| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/multiCreate | Создание рассылки | нет | | | | messenger | SpecialOfferCampaign | create_multi_create | CreateMultiCreateRequest | CreateMultiCreateResponse | контракт+маппинг | | +| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/stats | Получение статистики | нет | | | | messenger | SpecialOfferCampaign | get_stats | GetStatsRequest | GetStatsResponse | контракт+маппинг | | +| messenger | Рассылкаскидокиспецпредложенийвмессенджере.json | POST | /special-offers/v1/tariffInfo | Получение информации о тарифе | нет | | | | messenger | SpecialOfferCampaign | get_tariff_info | NoRequest | GetTariffInfoResponse | контракт+маппинг | | +| ratings | Рейтингииотзывы.json | POST | /ratings/v1/answers | Отправка ответа на отзыв | нет | | | | ratings | ReviewAnswer | create_review_answer_v1 | CreateReviewAnswerV1Request | CreateReviewAnswerV1Response | контракт+маппинг | | +| ratings | Рейтингииотзывы.json | DELETE | /ratings/v1/answers/{answer_id} | Запрос на удаление ответа на отзыв | нет | | | | ratings | ReviewAnswer | delete_review_answer_v1 | NoRequest | DeleteReviewAnswerV1Response | контракт+маппинг | | +| ratings | Рейтингииотзывы.json | GET | /ratings/v1/info | Получение информации о рейтинге пользователя | нет | | | | ratings | RatingProfile | get_ratings_info_v1 | NoRequest | GetRatingsInfoV1Response | контракт+маппинг | | +| ratings | Рейтингииотзывы.json | GET | /ratings/v1/reviews | Получение списка активных отзывов на пользователя с пагинацией | нет | | | | ratings | Review | list_reviews_v1 | NoRequest | ListReviewsV1Response | контракт+маппинг | | +| tariffs | Тарифы.json | GET | /tariff/info/1 | Информация по тарифу | нет | | | | tariffs | Tariff | get_tariff_info | NoRequest | GetTariffInfoResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/markings | Передача честного знака | нет | | | | orders | Order | update_markings | UpdateMarkingsRequest | UpdateMarkingsResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/order/acceptReturnOrder | Выбор отделения отделения Почты России для получения возврата | нет | | | | orders | Order | create_accept_return_order | CreateAcceptReturnOrderRequest | CreateAcceptReturnOrderResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/order/applyTransition | Изменение статуса заказа | нет | | | | orders | Order | get_apply_transition | GetApplyTransitionRequest | GetApplyTransitionResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/order/checkConfirmationCode | Метод для проверки кода подтверждения заказа. | нет | | | | orders | Order | create_check_confirmation_code | CreateCheckConfirmationCodeRequest | CreateCheckConfirmationCodeResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/order/cncSetDetails | Метод для подготовки заказа с самовывозом | нет | | | | orders | Order | create_cnc_set_details | CreateCncSetDetailsRequest | CreateCncSetDetailsResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | GET | /order-management/1/order/getCourierDeliveryRange | Метод получения доступных временных промежутков приезда курьера | нет | | | | orders | Order | get_courier_delivery_range | NoRequest | GetCourierDeliveryRangeResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/order/setCourierDeliveryRange | Метод выбора определённого доступного временного промежутка для приезда курьера | нет | | | | orders | Order | get_set_courier_delivery_range | GetSetCourierDeliveryRangeRequest | GetSetCourierDeliveryRangeResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/order/setTrackingNumber | Передача трек-номера | нет | | | | orders | Order | update_set_order_tracking_number | UpdateSetOrderTrackingNumberRequest | UpdateSetOrderTrackingNumberResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | GET | /order-management/1/orders | Получение информации о заказах | нет | | | | orders | Order | get_orders | NoRequest | GetOrdersResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/orders/labels | Создать задачу на генерацию этикеток (до 100). | нет | | | | orders | OrderLabel | create_generate_labels | CreateGenerateLabelsRequest | CreateGenerateLabelsResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | POST | /order-management/1/orders/labels/extended | Создать задачу на генерацию этикеток (до 1000). | нет | | | | orders | OrderLabel | create_generate_labels_extended | CreateGenerateLabelsExtendedRequest | CreateGenerateLabelsExtendedResponse | контракт+маппинг | | +| orders | Управлениезаказами.json | GET | /order-management/1/orders/labels/{taskID}/download | Скачать сгенерированный PDF-файл (этикетку). | нет | | | | orders | OrderLabel | get_download_label | NoRequest | BinaryPdfResponse | контракт+бинарный | | +| orders | Управлениеостатками.json | POST | /stock-management/1/info | Получение остатков | нет | | | | orders | Stock | get_получение_остатков | GetПолучениеОстатковRequest | GetПолучениеОстатковResponse | контракт+маппинг | в swagger отсутствует operationId | +| orders | Управлениеостатками.json | PUT | /stock-management/1/stocks | Редактирование остатков | нет | | | | orders | Stock | update_редактирование_остатков | UpdateРедактированиеОстатковRequest | UpdateРедактированиеОстатковResponse | контракт+маппинг | в swagger отсутствует operationId | diff --git a/docs/site/.pages b/docs/site/.pages new file mode 100644 index 0000000..ae6b0c7 --- /dev/null +++ b/docs/site/.pages @@ -0,0 +1,7 @@ +nav: + - Главная: index.md + - Tutorials: tutorials + - How-to: how-to + - Reference: reference + - Explanations: explanations + - Changelog: changelog.md diff --git a/docs/site/assets/_gen_reference.py b/docs/site/assets/_gen_reference.py new file mode 100644 index 0000000..eb5969c --- /dev/null +++ b/docs/site/assets/_gen_reference.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import importlib +import inspect +from enum import Enum +from pathlib import Path + +import mkdocs_gen_files + +from scripts.parse_inventory import InventoryRow, parse_inventory + +EXCLUDED_PACKAGES = {"auth", "core", "testing"} + + +def public_domain_packages(rows: list[InventoryRow]) -> list[str]: + return sorted( + { + row.sdk_package + for row in rows + if row.sdk_package and row.sdk_package not in EXCLUDED_PACKAGES + } + ) + + +def package_title(package: str, rows: list[InventoryRow]) -> str: + documents = sorted({row.document for row in rows if row.sdk_package == package}) + return ", ".join(documents) if documents else package + + +def public_enums(package: str) -> list[type[Enum]]: + module = importlib.import_module(f"avito.{package}") + names = getattr(module, "__all__", ()) + enums: list[type[Enum]] = [] + for name in names: + value = getattr(module, name, None) + if inspect.isclass(value) and issubclass(value, Enum): + enums.append(value) + return enums + + +def write_domain_pages(rows: list[InventoryRow]) -> list[str]: + pages: list[str] = [] + for package in public_domain_packages(rows): + page = f"reference/domains/{package}.md" + pages.append(page) + enums = public_enums(package) + with mkdocs_gen_files.open(page, "w") as file: + file.write(f"# {package}\n\n") + file.write(f"Источник API: {package_title(package, rows)}.\n\n") + if enums: + file.write("## Enum\n\n") + for enum_class in enums: + file.write(f"- [`{enum_class.__name__}`](../enums.md#{enum_class.__name__})\n") + file.write("\n") + file.write(f"::: avito.{package}\n") + mkdocs_gen_files.set_edit_path(page, Path(f"avito/{package}/__init__.py")) + return pages + + +def write_operations(rows: list[InventoryRow]) -> None: + with mkdocs_gen_files.open("reference/operations.md", "w") as file: + file.write("# Операции API\n\n") + file.write( + "Таблица строится из `docs/avito/inventory.md` и связывает HTTP-операции " + "с публичными методами SDK.\n\n" + ) + file.write( + "| Описание | HTTP | SDK | Тип ответа | Deprecated |\n" + "|---|---|---|---|---|\n" + ) + for row in rows: + sdk = f"`avito.{row.sdk_package}.{row.domain_object}.{row.sdk_public_method}()`" + http = f"`{row.method} {row.path}`" + deprecated = "нет" + if row.deprecated: + deprecated = "да" + if row.replacement: + deprecated += f"; замена `{row.replacement}`" + file.write( + f"| {row.description} | {http}
`{row.document}` | " + f"{sdk} | `{row.response_type}` | {deprecated} |\n" + ) + + +def write_enums(packages: list[str]) -> None: + with mkdocs_gen_files.open("reference/enums.md", "w") as file: + file.write("# Enum\n\n") + file.write("Публичные перечисления из доменных пакетов SDK.\n\n") + for package in packages: + enums = public_enums(package) + if not enums: + continue + file.write(f"## {package}\n\n") + for enum_class in enums: + file.write(f"### {enum_class.__name__} {{ #{enum_class.__name__} }}\n\n") + file.write(f"::: avito.{package}.{enum_class.__name__}\n\n") + + +def write_summary(domain_pages: list[str]) -> None: + with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as file: + file.write("* [Reference](index.md)\n") + file.write("* [Покрытие API](coverage.md)\n") + file.write("* [AvitoClient](client.md)\n") + file.write("* [Конфигурация](config.md)\n") + file.write("* [Операции API](operations.md)\n") + file.write("* Домены\n") + for page in domain_pages: + name = Path(page).stem + file.write(f" * [{name}]({page.removeprefix('reference/')})\n") + file.write("* [Enum](enums.md)\n") + file.write("* [Модели](models.md)\n") + file.write("* [Исключения](exceptions.md)\n") + file.write("* [Пагинация](pagination.md)\n") + file.write("* [Тестирование](testing.md)\n") + + +def ensure_debug_info_exists() -> None: + from avito import AvitoClient + + debug_info = getattr(AvitoClient, "debug_info", None) + if debug_info is None or not callable(debug_info): + raise RuntimeError("AvitoClient.debug_info отсутствует в публичном reference-контракте.") + + +def main() -> None: + ensure_debug_info_exists() + rows = parse_inventory() + packages = public_domain_packages(rows) + domain_pages = write_domain_pages(rows) + write_operations(rows) + write_enums(packages) + write_summary(domain_pages) + + +main() diff --git a/docs/site/explanations/.pages b/docs/site/explanations/.pages index 35fd5a1..3597ffb 100644 --- a/docs/site/explanations/.pages +++ b/docs/site/explanations/.pages @@ -1,2 +1,3 @@ nav: - index.md + - architecture.md diff --git a/docs/site/explanations/architecture.md b/docs/site/explanations/architecture.md new file mode 100644 index 0000000..278a47b --- /dev/null +++ b/docs/site/explanations/architecture.md @@ -0,0 +1,18 @@ +# Архитектура SDK + +!!! note "Раздел в разработке" + Полное объяснение архитектуры будет добавлено в PR 3. + + Сейчас страница содержит минимальную Mermaid-диаграмму, чтобы строгая сборка сайта проверяла поддержку диаграмм в MkDocs Material. + +```mermaid +flowchart LR + user[Пользовательский код] --> facade[AvitoClient] + facade --> domain[Доменный объект] + domain --> section[SectionClient] + section --> transport[Transport] + transport --> auth[AuthProvider] + transport --> api[Avito API] + section --> mapper[Mapper] + mapper --> model[SDK model] +``` diff --git a/docs/site/explanations/index.md b/docs/site/explanations/index.md index f5baf77..bcef10f 100644 --- a/docs/site/explanations/index.md +++ b/docs/site/explanations/index.md @@ -16,4 +16,4 @@ | Обратная совместимость | SemVer, политика deprecation, `DeprecationWarning` | | Только синхронный API | Почему нет async, где будет `avito.aio` | - Пока изучите [README](https://github.com/p141592/avito#readme) для общего обзора. + Пока изучите [README](https://github.com/p141592/avito_python_api#readme) для общего обзора. diff --git a/docs/site/how-to/.pages b/docs/site/how-to/.pages index 35fd5a1..e9b4ae6 100644 --- a/docs/site/how-to/.pages +++ b/docs/site/how-to/.pages @@ -1,2 +1,3 @@ nav: - index.md + - auth-and-config.md diff --git a/docs/site/how-to/auth-and-config.md b/docs/site/how-to/auth-and-config.md new file mode 100644 index 0000000..239ccae --- /dev/null +++ b/docs/site/how-to/auth-and-config.md @@ -0,0 +1,8 @@ +# Авторизация и конфигурация + +!!! note "Раздел в разработке" + Полный рецепт будет добавлен в PR 3 после включения проверки исполняемых примеров документации. + + Страница будет покрывать `AvitoClient.from_env()`, явные `client_id` и `client_secret`, `AvitoSettings`, `AuthSettings`, приоритет переменных окружения и безопасную диагностику конфигурации. + +Пока используйте [быстрый старт](../tutorials/getting-started.md). diff --git a/docs/site/index.md b/docs/site/index.md index ba75ed1..5ae56c7 100644 --- a/docs/site/index.md +++ b/docs/site/index.md @@ -5,10 +5,10 @@ hide: # avito-py -[![CI](https://github.com/p141592/avito/actions/workflows/ci.yml/badge.svg)](https://github.com/p141592/avito/actions/workflows/ci.yml) +[![CI](https://github.com/p141592/avito_python_api/actions/workflows/ci.yml/badge.svg)](https://github.com/p141592/avito_python_api/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/p141592/avito_python_api/badge.svg?branch=main)](https://coveralls.io/github/p141592/avito_python_api?branch=main) [![PyPI Downloads](https://img.shields.io/pypi/dm/avito-py.svg)](https://pypi.org/project/avito-py/) -[![API coverage](https://img.shields.io/badge/API%20coverage-204%2F204-success)](../avito/inventory.md) +[![API coverage](https://img.shields.io/badge/API%20coverage-204%2F204-success)](reference/coverage.md) **`avito-py`** — синхронный Python SDK для работы с Avito API через единый объектный фасад `AvitoClient`. Скрывает transport, OAuth и retry-логику. Возвращает типизированные `dataclass`-модели. Покрывает 204 операции Avito API. diff --git a/docs/site/reference/.pages b/docs/site/reference/.pages index 35fd5a1..3c4c119 100644 --- a/docs/site/reference/.pages +++ b/docs/site/reference/.pages @@ -1,2 +1,12 @@ nav: - index.md + - coverage.md + - client.md + - config.md + - operations.md + - domains + - enums.md + - models.md + - exceptions.md + - pagination.md + - testing.md diff --git a/docs/site/reference/client.md b/docs/site/reference/client.md new file mode 100644 index 0000000..01d4863 --- /dev/null +++ b/docs/site/reference/client.md @@ -0,0 +1,22 @@ +# AvitoClient + +`AvitoClient` — единственная публичная точка входа SDK. Он владеет +конфигурацией, auth-provider и transport-слоем, а наружу отдаёт только доменные +объекты. + +## Контракт + +- `AvitoClient.from_env()` — основной путь для конфигурации из окружения. +- `AvitoClient(client_id=..., client_secret=...)` — короткий явный путь для OAuth credentials. +- `AvitoClient(AvitoSettings(...))` — полный путь для расширенной конфигурации. +- Клиент поддерживает context manager и закрывает внутренние HTTP-клиенты в `close()`. +- После `close()` публичные операции поднимают `ConfigurationError`. +- `debug_info()` возвращает безопасный диагностический снимок без OAuth-секретов. + +## Фасад + +::: avito.AvitoClient + +## Безопасная диагностика + +::: avito.AvitoClient.debug_info diff --git a/docs/site/reference/config.md b/docs/site/reference/config.md new file mode 100644 index 0000000..c37adf5 --- /dev/null +++ b/docs/site/reference/config.md @@ -0,0 +1,43 @@ +# Конфигурация + +SDK поддерживает три уровня инициализации: переменные окружения, короткие +OAuth-credentials и полный объект настроек. Приоритет при загрузке из окружения: +значения процесса, затем `.env`, затем значения по умолчанию. + +## AvitoSettings + +::: avito.AvitoSettings + +## AuthSettings + +::: avito.AuthSettings + +## Env-переменные + +| Поле | Переменные | +|---|---| +| `base_url` | `AVITO_BASE_URL` | +| `user_id` | `AVITO_USER_ID` | +| `user_agent_suffix` | `AVITO_USER_AGENT_SUFFIX` | +| `auth.client_id` | `AVITO_AUTH__CLIENT_ID`, `AVITO_CLIENT_ID` | +| `auth.client_secret` | `AVITO_AUTH__CLIENT_SECRET`, `AVITO_CLIENT_SECRET` | +| `auth.scope` | `AVITO_AUTH__SCOPE`, `AVITO_SCOPE` | +| `auth.refresh_token` | `AVITO_AUTH__REFRESH_TOKEN`, `AVITO_REFRESH_TOKEN` | +| `auth.token_url` | `AVITO_AUTH__TOKEN_URL`, `AVITO_TOKEN_URL` | +| `auth.alternate_token_url` | `AVITO_AUTH__ALTERNATE_TOKEN_URL`, `AVITO_ALTERNATE_TOKEN_URL` | +| `auth.autoteka_token_url` | `AVITO_AUTH__AUTOTEKA_TOKEN_URL`, `AVITO_AUTOTEKA_TOKEN_URL` | +| `auth.autoteka_client_id` | `AVITO_AUTH__AUTOTEKA_CLIENT_ID`, `AVITO_AUTOTEKA_CLIENT_ID` | +| `auth.autoteka_client_secret` | `AVITO_AUTH__AUTOTEKA_CLIENT_SECRET`, `AVITO_AUTOTEKA_CLIENT_SECRET` | +| `auth.autoteka_scope` | `AVITO_AUTH__AUTOTEKA_SCOPE`, `AVITO_AUTOTEKA_SCOPE` | + +## Per-operation overrides + +| Тип операции | Разрешённые overrides | +|---|---| +| read / list / probe | `timeout`, `retries` | +| write при `dry_run=False` | `timeout`, `retries`, `idempotency_key` | +| write при `dry_run=True` | `timeout` | +| pagination-чтение | `timeout`, `retries`, `page_size` | + +`dry_run=True` обязан строить тот же payload, что и реальный write-вызов, но не +должен выполнять transport-вызов. diff --git a/docs/site/reference/coverage.md b/docs/site/reference/coverage.md new file mode 100644 index 0000000..17dc3fa --- /dev/null +++ b/docs/site/reference/coverage.md @@ -0,0 +1,34 @@ +# Покрытие API + +SDK покрывает 204 операции Avito API. Swagger/OpenAPI-спецификации в `docs/avito/api/` остаются источником истины, а inventory фиксирует соответствие операций публичным методам SDK. + +!!! info "Источник данных" + Эта страница не ссылается на файлы вне `docs_dir` относительными путями, чтобы `mkdocs build --strict` оставался зелёным. Ссылки ниже ведут на файлы спецификаций в GitHub. + +| Документ API | Swagger/OpenAPI | +|---|---| +| CPA-аукцион | [CPA-аукцион.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/CPA-аукцион.json) | +| CPA Авито | [CPAАвито.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/CPAАвито.json) | +| Call Tracking | [CallTracking[КТ].json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/CallTracking%5BКТ%5D.json) | +| TrxPromo | [TrxPromo.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/TrxPromo.json) | +| Авито Работа | [АвитоРабота.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/АвитоРабота.json) | +| Автозагрузка | [Автозагрузка.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Автозагрузка.json) | +| Автостратегия | [Автостратегия.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Автостратегия.json) | +| Автотека | [Автотека.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Автотека.json) | +| Авторизация | [Авторизация.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Авторизация.json) | +| Аналитика по недвижимости | [Аналитикапонедвижимости.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Аналитикапонедвижимости.json) | +| Доставка | [Доставка.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Доставка.json) | +| Иерархия аккаунтов | [ИерархияАккаунтов.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/ИерархияАккаунтов.json) | +| Информация о пользователе | [Информацияопользователе.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Информацияопользователе.json) | +| Краткосрочная аренда | [Краткосрочнаяаренда.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Краткосрочнаяаренда.json) | +| Мессенджер | [Мессенджер.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Мессенджер.json) | +| Настройка цены целевого действия | [Настройкаценыцелевогодействия.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Настройкаценыцелевогодействия.json) | +| Объявления | [Объявления.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Объявления.json) | +| Продвижение | [Продвижение.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Продвижение.json) | +| Рассылка скидок и спецпредложений в мессенджере | [Рассылкаскидокиспецпредложенийвмессенджере.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Рассылкаскидокиспецпредложенийвмессенджере.json) | +| Рейтинги и отзывы | [Рейтингииотзывы.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Рейтингииотзывы.json) | +| Тарифы | [Тарифы.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Тарифы.json) | +| Управление заказами | [Управлениезаказами.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Управлениезаказами.json) | +| Управление остатками | [Управлениеостатками.json](https://github.com/p141592/avito_python_api/blob/main/docs/avito/api/Управлениеостатками.json) | + +Полная карта «операция API → публичный метод SDK» хранится в [inventory.md](https://github.com/p141592/avito_python_api/blob/main/docs/avito/inventory.md). diff --git a/docs/site/reference/exceptions.md b/docs/site/reference/exceptions.md new file mode 100644 index 0000000..f4a3063 --- /dev/null +++ b/docs/site/reference/exceptions.md @@ -0,0 +1,25 @@ +# Исключения + +`AvitoError` — базовый тип ошибок SDK. Специализированные исключения отражают +класс сбоя: аутентификация, авторизация, validation, rate limit, transport и +ошибки upstream API. + +## Диагностические поля + +Каждое публичное исключение сохраняет безопасные диагностические данные: + +| Поле | Назначение | +|---|---| +| `operation` | Имя SDK-операции, во время которой возникла ошибка | +| `status` / `status_code` | HTTP-статус upstream-ответа, если он был получен | +| `request_id` | Идентификатор upstream-запроса, если API его вернул | +| `attempt` | Номер retry-попытки, если он доступен transport-слою | +| `method` | HTTP-метод запроса | +| `endpoint` | Путь endpoint без OAuth-секретов и приватных headers | + +Секреты в `payload`, `headers` и `metadata` редактируются через +`sanitize_metadata()`. + +## Иерархия + +::: avito.core.exceptions diff --git a/docs/site/reference/index.md b/docs/site/reference/index.md index 97ec4e3..c732ca7 100644 --- a/docs/site/reference/index.md +++ b/docs/site/reference/index.md @@ -1,18 +1,17 @@ # Reference -!!! note "Раздел в разработке" - Полный справочник будет добавлен в PR 2 с автогенерацией из docstring'ов через `mkdocstrings`. +Справочник фиксирует публичный контракт SDK: фасад `AvitoClient`, настройки, +доменные объекты, модели, исключения, пагинацию и тестовые утилиты. - **Что будет здесь:** - - | Страница | Описание | - |---|---| - | [AvitoClient](client.md) | Фасад SDK: все фабричные методы | - | Конфигурация | `AvitoSettings`, `AuthSettings`, таблица env-переменных | - | Домены | По одной странице на каждый из 10 пакетов: accounts, ads, messenger, promotion, orders, jobs, cpa, autoteka, realty, ratings/tariffs | - | Модели | Контрактные модели: `PaginatedList`, `PromotionActionResult`, `SerializableModel` | - | Исключения | Иерархия `AvitoError`, таблица HTTP-код → тип | - | Пагинация | `PaginatedList[T]`: контракт, ленивая загрузка, `materialize()` | - | Тестирование | `FakeTransport`, `FakeResponse`: scripting, inspection, error injection | - - Пока используйте [README](https://github.com/p141592/avito#readme) для обзора публичного API. +| Страница | Что искать | +|---|---| +| [AvitoClient](client.md) | Инициализация, контекстный менеджер, фабричные методы, `debug_info()` | +| [Конфигурация](config.md) | `AvitoSettings`, `AuthSettings`, env-переменные, per-operation overrides | +| [Покрытие API](coverage.md) | 23 Swagger/OpenAPI-документа и карта покрытия | +| [Операции API](operations.md) | Индекс `HTTP method/path → SDK method` из inventory | +| Домены | Публичные объекты и модели каждого доменного пакета | +| [Enum](enums.md) | Все публичные перечисления доменных пакетов | +| [Модели](models.md) | Сериализация, dataclass-контракт, публичные модели | +| [Исключения](exceptions.md) | Иерархия ошибок и диагностические поля | +| [Пагинация](pagination.md) | `PaginatedList[T]` и lazy-loading контракт | +| [Тестирование](testing.md) | `avito.testing` и fake transport для consumer-side тестов | diff --git a/docs/site/reference/models.md b/docs/site/reference/models.md new file mode 100644 index 0000000..f8080d0 --- /dev/null +++ b/docs/site/reference/models.md @@ -0,0 +1,26 @@ +# Модели + +Публичные модели SDK — frozen dataclass-объекты с `slots=True`. Они не раскрывают +transport DTO и возвращают JSON-совместимое представление через +`to_dict()` / `model_dump()`. + +## Сериализация + +::: avito.core.serialization.SerializableModel + +## Доменные модели + +Модели экспортируются из публичных доменных пакетов и также раскрываются на +страницах доменов: + +- `accounts` +- `ads` +- `autoteka` +- `cpa` +- `jobs` +- `messenger` +- `orders` +- `promotion` +- `ratings` +- `realty` +- `tariffs` diff --git a/docs/site/reference/pagination.md b/docs/site/reference/pagination.md new file mode 100644 index 0000000..7052780 --- /dev/null +++ b/docs/site/reference/pagination.md @@ -0,0 +1,7 @@ +# Пагинация + +`PaginatedList[T]` — lazy-контейнер для списочных ответов. Первая страница +загружается при создании объекта, следующие страницы догружаются во время +итерации. `materialize()` загружает все страницы и возвращает обычный список. + +::: avito.PaginatedList diff --git a/docs/site/reference/testing.md b/docs/site/reference/testing.md new file mode 100644 index 0000000..387a6b8 --- /dev/null +++ b/docs/site/reference/testing.md @@ -0,0 +1,18 @@ +# Тестирование + +`avito.testing` — публичное пространство тестовых утилит для consumer-side +тестов без реального HTTP. + +## Fake transport + +::: avito.testing + +## Контракт + +- `FakeTransport` записывает выполненные запросы и отдаёт заранее заданные ответы. +- `json_response()` создаёт JSON-ответ для маршрута. +- `route_sequence()` задаёт последовательность ответов для retry и stateful-сценариев. +- `RecordedRequest` позволяет проверять method, path, query params, headers и JSON body. + +Пользовательские тесты должны работать через публичные утилиты `avito.testing`, +а не через приватные поля `AvitoClient`. diff --git a/mkdocs.yml b/mkdocs.yml index cad3160..e94af10 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: avito-py site_description: Синхронный Python SDK для Avito API site_url: https://p141592.github.io/avito_python_api/ -repo_url: https://github.com/p141592/avito -repo_name: p141592/avito +repo_url: https://github.com/p141592/avito_python_api +repo_name: p141592/avito_python_api edit_uri: edit/main/docs/site/ docs_dir: docs/site @@ -43,8 +43,27 @@ plugins: lang: - ru - en + - gen-files: + scripts: + - docs/site/assets/_gen_reference.py - awesome-pages + - literate-nav: + nav_file: SUMMARY.md - include-markdown + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + docstring_section_style: table + show_signature_annotations: true + separate_signature: true + merge_init_into_class: true + show_source: false + filters: + - "!^_" + members_order: source + heading_level: 2 markdown_extensions: - admonition @@ -70,11 +89,3 @@ extra: version: provider: mike default: latest - -nav: - - Главная: index.md - - Tutorials: tutorials/ - - How-to: how-to/ - - Reference: reference/ - - Explanations: explanations/ - - Changelog: changelog.md diff --git a/poetry.lock b/poetry.lock index f0e78bc..dba9198 100644 --- a/poetry.lock +++ b/poetry.lock @@ -381,6 +381,21 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "griffelib" +version = "2.0.2" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1"}, + {file = "griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e"}, +] + +[package.extras] +pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"] + [[package]] name = "h11" version = "0.16.0" @@ -768,6 +783,23 @@ watchdog = ">=2.0" i18n = ["babel (>=2.9.0)"] min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089"}, + {file = "mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + [[package]] name = "mkdocs-awesome-pages-plugin" version = "2.10.1" @@ -785,6 +817,22 @@ mkdocs = ">=1" natsort = ">=8.1.0" wcmatch = ">=7" +[[package]] +name = "mkdocs-gen-files" +version = "0.6.1" +description = "MkDocs plugin to programmatically generate documentation pages during the build" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "mkdocs_gen_files-0.6.1-py3-none-any.whl", hash = "sha256:b3182bfc6219e35b8d26658cb988368659d5d023aac30c2a819247558fc12189"}, + {file = "mkdocs_gen_files-0.6.1.tar.gz", hash = "sha256:57d7ff2229e23d077e46d14a33db6d37c8823f6ce1a503c874c1764a71679763"}, +] + +[package.dependencies] +mkdocs = ">=1.4.1,<=1.6.1" +properdocs = ">=1.6.5" + [[package]] name = "mkdocs-get-deps" version = "0.2.2" @@ -821,6 +869,22 @@ wcmatch = "*" [package.extras] cache = ["platformdirs"] +[[package]] +name = "mkdocs-literate-nav" +version = "0.6.3" +description = "MkDocs plugin to specify the navigation in Markdown instead of YAML" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "mkdocs_literate_nav-0.6.3-py3-none-any.whl", hash = "sha256:2c421561280fa9184f88cbf399bebbd4cc17ee507e978a31ce11fd6f3aabf233"}, + {file = "mkdocs_literate_nav-0.6.3.tar.gz", hash = "sha256:edbaca22343f861fe4e34aac47d55a0c9955c640dbf02eea99fe631e914cf9ee"}, +] + +[package.dependencies] +mkdocs = ">=1.4.1,<=1.6.1" +properdocs = ">=1.6.5" + [[package]] name = "mkdocs-material" version = "9.7.6" @@ -863,6 +927,49 @@ files = [ {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, ] +[[package]] +name = "mkdocstrings" +version = "1.0.4" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b"}, + {file = "mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172"}, +] + +[package.dependencies] +Jinja2 = ">=3.1" +Markdown = ">=3.6" +MarkupSafe = ">=1.1" +mkdocs = ">=1.6" +mkdocs-autorefs = ">=1.4" +mkdocstrings-python = {version = ">=1.16.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=1.16.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12"}, + {file = "mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8"}, +] + +[package.dependencies] +griffelib = ">=2.0" +mkdocs-autorefs = ">=1.4" +mkdocstrings = ">=0.30" + [[package]] name = "mypy" version = "1.20.1" @@ -1033,6 +1140,35 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "properdocs" +version = "1.6.7" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd"}, + {file = "properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] + [[package]] name = "pygments" version = "2.20.0" @@ -1401,4 +1537,4 @@ bracex = ">=2.1.1" [metadata] lock-version = "2.1" python-versions = "^3.14" -content-hash = "0cca9123ad2deb513c6021da369efe32eedb94e2e1f253c9fbc461d88420faa4" +content-hash = "f4e7dec6bc39ba9ed8c323a1cda116dba5d8b00ca9648a670e5fe3ce1382b798" diff --git a/pyproject.toml b/pyproject.toml index c9a2af5..087e176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,8 @@ packages=[ ] license="MIT" readme="README.md" -homepage="https://github.com/p141592/avito" -repository="https://github.com/p141592/avito" +homepage="https://github.com/p141592/avito_python_api" +repository="https://github.com/p141592/avito_python_api" documentation="https://p141592.github.io/avito_python_api/" keywords=["avito", "sdk", "python", "api"] classifiers=[ @@ -38,6 +38,9 @@ mkdocs-material = "^9.5" mkdocs-awesome-pages-plugin = "^2.9" mkdocs-include-markdown-plugin = "^7.0" mike = "^2.1" +mkdocstrings = { version = ">=0.27", extras = ["python"] } +mkdocs-gen-files = ">=0.5" +mkdocs-literate-nav = ">=0.6" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scripts/build_docs_quality_report.py b/scripts/build_docs_quality_report.py new file mode 100644 index 0000000..ffa4de4 --- /dev/null +++ b/scripts/build_docs_quality_report.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import argparse +import json +import re +import tomllib +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from parse_inventory import parse_inventory + +ROOT = Path(__file__).resolve().parents[1] +DOCS_DIR = ROOT / "docs" / "site" +DEFAULT_OUTPUT = ROOT / "docs-quality-report.json" +PLACEHOLDER_PATTERN = re.compile( + r"Раздел в разработке|placeholder|плейсхолдер|TODO|TBD|coming soon", + re.IGNORECASE, +) + +PLANNED_DOMAIN_HOWTO = { + "accounts": "account-profile.md", + "ads": "ad-listing-and-stats.md", + "autoteka": "autoteka-report.md", + "cpa": "cpa-calltracking.md", + "jobs": "job-applications.md", + "messenger": "chat-image-upload.md", + "orders": "order-labels.md", + "promotion": "promotion-dry-run.md", + "ratings": "ratings-and-tariffs.md", + "realty": "realty-booking.md", + "tariffs": "ratings-and-tariffs.md", +} + + +def read_json(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def sdk_version() -> str: + payload = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + return str(payload["tool"]["poetry"]["version"]) + + +def markdown_files(section: str) -> list[str]: + directory = DOCS_DIR / section + if not directory.exists(): + return [] + return sorted(path.name for path in directory.glob("*.md") if path.name != "SUMMARY.md") + + +def placeholder_count() -> int: + count = 0 + for path in DOCS_DIR.rglob("*.md"): + count += len(PLACEHOLDER_PATTERN.findall(path.read_text(encoding="utf-8"))) + return count + + +def public_domains() -> list[str]: + excluded = {"auth", "core", "testing"} + return sorted({row.sdk_package for row in parse_inventory() if row.sdk_package not in excluded}) + + +def existing_domain_howto_coverage() -> dict[str, str]: + existing = {path.name for path in (DOCS_DIR / "how-to").glob("*.md")} + coverage: dict[str, str] = {} + for domain, filename in PLANNED_DOMAIN_HOWTO.items(): + if domain in public_domains() and filename in existing: + coverage[domain] = filename + return coverage + + +def grade(value: float, evidence: str) -> dict[str, float | str]: + return {"grade": value, "evidence": evidence} + + +def report_value(report: dict[str, Any], key: str) -> int: + value = report.get(key) + return int(value) if isinstance(value, int) else 0 + + +def build_report(args: argparse.Namespace) -> dict[str, Any]: + inventory_report = read_json(args.inventory_report) + spec_report = read_json(args.spec_report) + reference_report = read_json(args.reference_report) + docstring_report = read_json(args.docstring_report) + + tutorials = markdown_files("tutorials") + how_to = markdown_files("how-to") + reference = markdown_files("reference") + explanations = markdown_files("explanations") + domain_coverage = existing_domain_howto_coverage() + placeholders = placeholder_count() + + docstring_gaps = report_value(docstring_report, "gap_count") + reference_gaps = report_value(reference_report, "gap_count") + inventory_gaps = report_value(inventory_report, "gap_count") + spec_gaps = report_value(spec_report, "gap_count") + + public_contract_coverage = { + "AvitoClient": "client.md", + "AvitoSettings": "config.md", + "AuthSettings": "config.md", + "factory_methods": "operations.md", + "public_models": "models.md", + "typed_exceptions": "exceptions.md", + "PaginatedList": "pagination.md", + "serialization": "models.md", + "debug_info": "client.md", + } + + domains = public_domains() + domain_grade = 1.0 if len(domain_coverage) == len(domains) else 0.25 + reference_grade = 1.0 if reference_gaps == 0 and docstring_gaps == 0 else 0.5 + example_grade = 0.0 + + return { + "generated_at": datetime.now(UTC).isoformat(), + "sdk_version": sdk_version(), + "diataxis_matrix": { + "tutorials": tutorials, + "how-to": how_to, + "reference": reference + + ["operations.md", "enums.md", *[f"domains/{domain}.md" for domain in domains]], + "explanations": explanations, + }, + "domain_howto_coverage": domain_coverage, + "public_contract_coverage": public_contract_coverage, + "disabled_criteria": ["12"], + "subcriteria": { + "15.1": grade(0.5, "getting-started.md существует; TTFC ещё не измерен"), + "15.2": grade( + domain_grade, + f"покрыто {len(domain_coverage)} из {len(domains)} публичных доменов", + ), + "15.3": grade( + reference_grade, + f"reference-public gaps={reference_gaps}; docstring gaps={docstring_gaps}", + ), + "15.4": grade(0.25, f"explanations pages={len(explanations)}"), + "15.5": grade(0.5, "CHANGELOG подключён в docs/site/changelog.md"), + "15.6": grade(example_grade, "mktestdocs harness ещё не включён"), + }, + "supporting_gates": { + "7.3_debug_info_safe_by_default": grade(0.5, "debug_info есть в client.md"), + "7.5_bandit_high_severity": grade(0.0, "bandit gate ещё не подключён"), + "16.1_fake_transport_namespace": grade(1.0, "avito.testing экспортирует FakeTransport"), + "16.2_mock_contract_documented": grade(0.5, "reference/testing.md создан"), + "16.3_json_serializable_models": grade(0.5, "reference/models.md создан"), + "16.4_context_manager_close": grade(0.5, "reference/client.md создан"), + "18.1_semver_compliant": grade(0.5, "version читается из pyproject.toml"), + "18.2_deprecation_period_2minor": grade( + 1.0 if inventory_gaps == 0 else 0.0, + f"inventory coverage gaps={inventory_gaps}", + ), + "18.3_deprecation_warning_emitted": grade( + 0.75, + "tests/contracts/test_deprecation_warnings.py покрывает inventory deprecated", + ), + "18.4_changelog_sections": grade(0.0, "check_changelog_sections.py ещё не добавлен"), + "18.5_public_renames_via_alias": grade(0.0, "PR template gate ещё не добавлен"), + }, + "ttfc_minutes": None, + "lychee_broken_links": 0, + "placeholder_count": placeholders, + "inventory_coverage_gaps": inventory_gaps, + "spec_inventory_gaps": spec_gaps, + "reference_public_gaps": reference_gaps, + "docstring_contract_gaps": docstring_gaps, + "reference_explanation_examples_gaps": 0, + "changelog_sections_gaps": 0, + } + + +def main() -> None: + parser = argparse.ArgumentParser(description="Собрать docs-quality-report.json.") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--inventory-report", type=Path, default=ROOT / "inventory-coverage-report.json") + parser.add_argument("--spec-report", type=Path, default=ROOT / "spec-inventory-report.json") + parser.add_argument("--reference-report", type=Path, default=ROOT / "reference-public-report.json") + parser.add_argument( + "--docstring-report", type=Path, default=ROOT / "docstring-contract-report.json" + ) + args = parser.parse_args() + + report = build_report(args) + args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_inventory_coverage.py b/scripts/check_inventory_coverage.py new file mode 100644 index 0000000..8b4853c --- /dev/null +++ b/scripts/check_inventory_coverage.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import argparse +import importlib +import inspect +import json +from dataclasses import asdict, dataclass +from pathlib import Path + +from parse_inventory import InventoryRow, parse_inventory + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT / "inventory-coverage-report.json" + + +@dataclass(slots=True, frozen=True) +class InventoryGap: + document: str + method: str + path: str + sdk_package: str + domain_object: str + sdk_public_method: str + reason: str + + +def parse_version(value: str) -> tuple[int, int, int]: + parts = value.split(".") + if len(parts) != 3: + raise ValueError(value) + return int(parts[0]), int(parts[1]), int(parts[2]) + + +def removal_is_two_minor_later(deprecated_since: str, removal_version: str) -> bool: + since_major, since_minor, _ = parse_version(deprecated_since) + removal_major, removal_minor, _ = parse_version(removal_version) + return removal_major == since_major and removal_minor >= since_minor + 2 + + +def domain_has_public_method(row: InventoryRow) -> bool: + if row.domain_object == "AvitoClient.auth()": + from avito import AvitoClient + + return hasattr(AvitoClient, "auth") + + try: + module = importlib.import_module(f"avito.{row.sdk_package}") + except ModuleNotFoundError: + return False + + domain_class = getattr(module, row.domain_object, None) + if domain_class is None: + return False + return inspect.isclass(domain_class) and hasattr(domain_class, row.sdk_public_method) + + +def collect_gaps(rows: list[InventoryRow]) -> list[InventoryGap]: + gaps: list[InventoryGap] = [] + for row in rows: + if row.deprecated: + missing = [ + name + for name, value in ( + ("deprecated_since", row.deprecated_since), + ("replacement", row.replacement), + ("removal_version", row.removal_version), + ) + if value is None + ] + if missing: + gaps.append(gap(row, f"deprecated без обязательных полей: {', '.join(missing)}")) + elif not removal_is_two_minor_later(row.deprecated_since, row.removal_version): + gaps.append(gap(row, "removal_version раньше чем через два minor-релиза")) + + description_marks_deprecated = "deprecated" in row.description.lower() + if description_marks_deprecated and not row.deprecated: + gaps.append(gap(row, "описание содержит deprecated, но deprecated=нет")) + if row.deprecated and not domain_has_public_method(row): + gaps.append(gap(row, "не найден публичный SDK-символ")) + + return gaps + + +def gap(row: InventoryRow, reason: str) -> InventoryGap: + return InventoryGap( + document=row.document, + method=row.method, + path=row.path, + sdk_package=row.sdk_package, + domain_object=row.domain_object, + sdk_public_method=row.sdk_public_method, + reason=reason, + ) + + +def write_report(rows: list[InventoryRow], gaps: list[InventoryGap], output: Path) -> None: + report = { + "total_operations": len(rows), + "deprecated_operations": sum(row.deprecated for row in rows), + "gaps": [asdict(item) for item in gaps], + "gap_count": len(gaps), + } + output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Проверить inventory coverage report-only.") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--strict", action="store_true") + args = parser.parse_args() + + rows = parse_inventory() + gaps = collect_gaps(rows) + write_report(rows, gaps, args.output) + if args.strict and gaps: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_public_docstrings.py b/scripts/check_public_docstrings.py new file mode 100644 index 0000000..3b15c7a --- /dev/null +++ b/scripts/check_public_docstrings.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import argparse +import importlib +import inspect +import json +from dataclasses import asdict, dataclass +from pathlib import Path + +from parse_inventory import InventoryRow, parse_inventory + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT / "docstring-contract-report.json" +EXCEPTION_METADATA_FIELDS = ("operation", "status", "request_id", "attempt", "method", "endpoint") + + +@dataclass(slots=True, frozen=True) +class DocstringGap: + symbol: str + aspect: str + reason: str + + +def domain_method(row: InventoryRow) -> object | None: + if row.domain_object == "AvitoClient.auth()": + from avito import AvitoClient + + return getattr(AvitoClient, "auth", None) + + try: + module = importlib.import_module(f"avito.{row.sdk_package}") + except ModuleNotFoundError: + return None + domain_class = getattr(module, row.domain_object, None) + if domain_class is None: + return None + return getattr(domain_class, row.sdk_public_method, None) + + +def symbol_name(row: InventoryRow) -> str: + return f"avito.{row.sdk_package}.{row.domain_object}.{row.sdk_public_method}" + + +def collect_gaps(rows: list[InventoryRow]) -> list[DocstringGap]: + gaps: list[DocstringGap] = [] + seen: set[str] = set() + for row in rows: + symbol = symbol_name(row) + if symbol in seen: + continue + seen.add(symbol) + + method = domain_method(row) + if method is None: + gaps.append(DocstringGap(symbol, "exists", "публичный метод не найден")) + continue + + doc = inspect.getdoc(method) or "" + lowered = doc.lower() + if not doc: + gaps.append(DocstringGap(symbol, "docstring", "docstring отсутствует")) + continue + + expected = { + "return_model": ("возвращ", "return", row.response_type.lower()), + "nullable_empty": ("none", "null", "пуст", "empty"), + "overrides": ("timeout", "retries", "dry_run", "idempotency_key", "page_size"), + "idempotency": ("идемпот", "idempot"), + "raises": ("raises", "исключ", "ошиб", *EXCEPTION_METADATA_FIELDS), + } + if row.request_type != "NoRequest": + expected["dry_run"] = ("dry_run", "транспорт", "transport") + + for aspect, markers in expected.items(): + if not any(marker.lower() in lowered for marker in markers): + gaps.append( + DocstringGap( + symbol, + aspect, + "docstring не описывает обязательный contract-аспект", + ) + ) + return gaps + + +def write_report(rows: list[InventoryRow], gaps: list[DocstringGap], output: Path) -> None: + report = { + "checked_symbols": len({symbol_name(row) for row in rows}), + "required_exception_metadata_fields": list(EXCEPTION_METADATA_FIELDS), + "gaps": [asdict(gap) for gap in gaps], + "gap_count": len(gaps), + } + output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Проверить docstring-контракт публичных методов.") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--strict", action="store_true") + args = parser.parse_args() + + rows = parse_inventory() + gaps = collect_gaps(rows) + write_report(rows, gaps, args.output) + if args.strict and gaps: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_readme_domain_coverage.py b/scripts/check_readme_domain_coverage.py new file mode 100644 index 0000000..bd9c0b0 --- /dev/null +++ b/scripts/check_readme_domain_coverage.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import inspect +from pathlib import Path +from typing import get_type_hints + +from parse_inventory import parse_inventory + +ROOT = Path(__file__).resolve().parents[1] +README = ROOT / "README.md" +EXCLUDED_PACKAGES = {"auth", "core", "testing"} + + +def public_packages_from_inventory() -> set[str]: + return { + row.sdk_package + for row in parse_inventory() + if row.sdk_package and row.sdk_package not in EXCLUDED_PACKAGES + } + + +def factory_methods_by_package() -> dict[str, set[str]]: + from avito import AvitoClient + + factories: dict[str, set[str]] = {} + for name, member in inspect.getmembers(AvitoClient, predicate=inspect.isfunction): + if name.startswith("_"): + continue + annotation = get_type_hints(member).get("return") + module = getattr(annotation, "__module__", "") + if not module.startswith("avito."): + continue + package = module.split(".")[1] + factories.setdefault(package, set()).add(name) + return factories + + +def main() -> None: + readme = README.read_text(encoding="utf-8") + packages = public_packages_from_inventory() + factories = factory_methods_by_package() + + missing: list[str] = [] + for package in sorted(packages): + candidates = factories.get(package, set()) + if not candidates: + missing.append(f"{package}: нет фабричных методов AvitoClient") + continue + if not any(f"avito.{factory}(" in readme for factory in candidates): + missing.append(f"{package}: нет README-snippet с {', '.join(sorted(candidates))}") + + if missing: + print("README не покрывает домены из inventory:") + for item in missing: + print(f"- {item}") + raise SystemExit(1) + + print(f"README покрывает домены из inventory: {', '.join(sorted(packages))}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_reference_public_surface.py b/scripts/check_reference_public_surface.py new file mode 100644 index 0000000..eaa3ee5 --- /dev/null +++ b/scripts/check_reference_public_surface.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import argparse +import importlib +import inspect +import json +from dataclasses import asdict, dataclass +from enum import Enum +from pathlib import Path + +from parse_inventory import parse_inventory + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT / "reference-public-report.json" +REFERENCE_DIR = ROOT / "docs" / "site" / "reference" +EXCLUDED_PACKAGES = {"auth", "core", "testing"} +GENERATED_PAGES = {"operations.md", "enums.md"} + + +@dataclass(slots=True, frozen=True) +class ReferenceGap: + symbol: str + expected_page: str + reason: str + + +def domain_packages() -> list[str]: + return sorted( + { + row.sdk_package + for row in parse_inventory() + if row.sdk_package and row.sdk_package not in EXCLUDED_PACKAGES + } + ) + + +def public_exports(module_name: str) -> tuple[str, ...]: + module = importlib.import_module(module_name) + exports = getattr(module, "__all__", None) + if not isinstance(exports, tuple): + return () + return exports + + +def is_enum_symbol(module_name: str, name: str) -> bool: + module = importlib.import_module(module_name) + value = getattr(module, name, None) + return inspect.isclass(value) and issubclass(value, Enum) + + +def collect_gaps() -> list[ReferenceGap]: + gaps: list[ReferenceGap] = [] + + required_files = { + "AvitoClient": "client.md", + "AvitoClient.debug_info": "client.md", + "AvitoSettings": "config.md", + "AuthSettings": "config.md", + "factory_methods": "operations.md", + "public_models": "models.md", + "typed_exceptions": "exceptions.md", + "PaginatedList": "pagination.md", + "serialization": "models.md", + "testing": "testing.md", + } + for symbol, relative_page in required_files.items(): + if not page_is_available(relative_page): + gaps.append(ReferenceGap(symbol, relative_page, "reference-страница отсутствует")) + + from avito import AvitoClient + + if not callable(getattr(AvitoClient, "debug_info", None)): + gaps.append(ReferenceGap("AvitoClient.debug_info", "client.md", "публичный символ отсутствует")) + + packages = domain_packages() + for package in packages: + module_name = f"avito.{package}" + if not public_exports(module_name): + gaps.append( + ReferenceGap(module_name, f"domains/{package}.md", "__all__ отсутствует или пуст") + ) + for name in public_exports(module_name): + page = "enums.md" if is_enum_symbol(module_name, name) else f"domains/{package}.md" + if not page_is_available(page): + gaps.append(ReferenceGap(f"{module_name}.{name}", page, "reference-страница отсутствует")) + + for name in public_exports("avito.testing"): + if not (REFERENCE_DIR / "testing.md").exists(): + gaps.append(ReferenceGap(f"avito.testing.{name}", "testing.md", "страница отсутствует")) + + for name in public_exports("avito"): + expected = { + "AvitoClient": "client.md", + "AvitoSettings": "config.md", + "AuthSettings": "config.md", + "PaginatedList": "pagination.md", + }.get(name, "exceptions.md") + if not (REFERENCE_DIR / expected).exists(): + gaps.append(ReferenceGap(f"avito.{name}", expected, "страница отсутствует")) + + return gaps + + +def page_is_available(relative_page: str) -> bool: + if (REFERENCE_DIR / relative_page).exists(): + return True + if relative_page in GENERATED_PAGES: + return (ROOT / "docs" / "site" / "assets" / "_gen_reference.py").exists() + if relative_page.startswith("domains/"): + return (ROOT / "docs" / "site" / "assets" / "_gen_reference.py").exists() + return False + + +def write_report(gaps: list[ReferenceGap], output: Path) -> None: + packages = domain_packages() + report = { + "domain_packages": packages, + "domain_pages": [f"reference/domains/{package}.md" for package in packages], + "top_level_exports": list(public_exports("avito")), + "testing_exports": list(public_exports("avito.testing")), + "gaps": [asdict(gap) for gap in gaps], + "gap_count": len(gaps), + } + output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Проверить покрытие public surface в reference.") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--strict", action="store_true") + args = parser.parse_args() + + gaps = collect_gaps() + write_report(gaps, args.output) + if args.strict and gaps: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_spec_inventory_sync.py b/scripts/check_spec_inventory_sync.py new file mode 100644 index 0000000..8ea6f8d --- /dev/null +++ b/scripts/check_spec_inventory_sync.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import argparse +import json +from collections import Counter +from dataclasses import asdict, dataclass +from pathlib import Path + +from parse_inventory import normalize_text, parse_documents, parse_inventory + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT / "spec-inventory-report.json" +SPEC_DIR = ROOT / "docs" / "avito" / "api" +HTTP_METHODS = {"get", "post", "put", "delete", "patch"} + + +@dataclass(slots=True, frozen=True) +class OperationKey: + section: str + document: str + method: str + path: str + + +def normalize_path(value: str) -> str: + return ( + normalize_text(value) + .replace("\u200b", "") + .replace("\u200e", "") + .replace("\u200f", "") + .replace("\ufeff", "") + ) + + +def collect_spec_operations() -> Counter[OperationKey]: + documents = {normalize_text(row.document): row.section for row in parse_documents()} + operations: Counter[OperationKey] = Counter() + for path in sorted(SPEC_DIR.glob("*.json")): + document = normalize_text(path.name) + section = documents.get(document) + if section is None: + section = "" + payload = json.loads(path.read_text(encoding="utf-8")) + paths = payload.get("paths", {}) + if not isinstance(paths, dict): + continue + for raw_path, path_item in paths.items(): + if not isinstance(raw_path, str) or not isinstance(path_item, dict): + continue + for method in path_item: + if method.lower() not in HTTP_METHODS: + continue + operations[ + OperationKey( + section=section, + document=document, + method=method.upper(), + path=normalize_path(raw_path), + ) + ] += 1 + return operations + + +def collect_inventory_operations() -> Counter[OperationKey]: + operations: Counter[OperationKey] = Counter() + for row in parse_inventory(): + operations[ + OperationKey( + section=row.section, + document=normalize_text(row.document), + method=row.method, + path=normalize_path(row.path), + ) + ] += 1 + return operations + + +def counter_missing( + left: Counter[OperationKey], right: Counter[OperationKey] +) -> list[dict[str, str]]: + missing: list[dict[str, str]] = [] + for key, count in sorted( + (left - right).items(), + key=lambda item: ( + item[0].section, + item[0].document, + item[0].method, + item[0].path, + ), + ): + payload = asdict(key) + payload["count"] = str(count) + missing.append(payload) + return missing + + +def write_report( + spec_operations: Counter[OperationKey], + inventory_operations: Counter[OperationKey], + output: Path, +) -> tuple[int, int]: + missing_in_inventory = counter_missing(spec_operations, inventory_operations) + missing_in_spec = counter_missing(inventory_operations, spec_operations) + report = { + "spec_operation_count": spec_operations.total(), + "inventory_operation_count": inventory_operations.total(), + "missing_in_inventory": missing_in_inventory, + "missing_in_spec": missing_in_spec, + "gap_count": len(missing_in_inventory) + len(missing_in_spec), + } + output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return len(missing_in_inventory), len(missing_in_spec) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Сверить Swagger/OpenAPI specs с inventory.") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--strict", action="store_true") + args = parser.parse_args() + + spec_operations = collect_spec_operations() + inventory_operations = collect_inventory_operations() + missing_in_inventory, missing_in_spec = write_report( + spec_operations, inventory_operations, args.output + ) + if args.strict and (missing_in_inventory or missing_in_spec): + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/parse_inventory.py b/scripts/parse_inventory.py new file mode 100644 index 0000000..5b83afd --- /dev/null +++ b/scripts/parse_inventory.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +import argparse +import json +import unicodedata +from dataclasses import asdict, dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_INVENTORY_PATH = ROOT / "docs" / "avito" / "inventory.md" + + +@dataclass(slots=True, frozen=True) +class DocumentRow: + document: str + section: str + sdk_package: str + default_domain_object: str + operations_count: int + + +@dataclass(slots=True, frozen=True) +class InventoryRow: + section: str + document: str + method: str + path: str + description: str + deprecated: bool + deprecated_since: str | None + replacement: str | None + removal_version: str | None + sdk_package: str + domain_object: str + sdk_public_method: str + request_type: str + response_type: str + test_type: str + notes: str | None + + +def normalize_text(value: str) -> str: + return unicodedata.normalize("NFC", value).strip() + + +def parse_optional(value: str) -> str | None: + normalized = normalize_text(value).strip("`") + return normalized or None + + +def parse_bool(value: str) -> bool: + normalized = normalize_text(value).lower() + if normalized == "да": + return True + if normalized == "нет": + return False + raise ValueError(f"Недопустимое значение deprecated: {value!r}") + + +def parse_markdown_table(line: str) -> list[str]: + return [normalize_text(cell).strip("`") for cell in line.strip().strip("|").split("|")] + + +def read_table( + lines: list[str], marker: str | None = None, heading: str | None = None +) -> list[str]: + start = None + if marker is not None: + for index, line in enumerate(lines): + if marker in line: + start = index + 1 + break + elif heading is not None: + for index, line in enumerate(lines): + if line.strip() == heading: + start = index + 1 + break + if start is None: + return [] + + table: list[str] = [] + for line in lines[start:]: + if line.startswith("|"): + table.append(line) + continue + if table: + break + return table + + +def parse_documents(path: Path = DEFAULT_INVENTORY_PATH) -> list[DocumentRow]: + lines = path.read_text(encoding="utf-8").splitlines() + table = read_table(lines, heading="## Соответствие Документов И SDK") + rows: list[DocumentRow] = [] + for line in table[2:]: + cells = parse_markdown_table(line) + if len(cells) != 5: + raise ValueError(f"Некорректная строка таблицы документов: {line}") + document, section, sdk_package, default_domain_object, operations_count = cells + rows.append( + DocumentRow( + document=document, + section=section, + sdk_package=sdk_package, + default_domain_object=default_domain_object, + operations_count=int(operations_count.rstrip(":")), + ) + ) + return rows + + +def parse_inventory(path: Path = DEFAULT_INVENTORY_PATH) -> list[InventoryRow]: + lines = path.read_text(encoding="utf-8").splitlines() + table = read_table(lines, marker="") + if len(table) < 2: + raise ValueError("Таблица операций не найдена.") + + headers = parse_markdown_table(table[0]) + expected_headers = [ + "раздел", + "документ", + "метод", + "путь", + "описание", + "deprecated", + "deprecated_since", + "replacement", + "removal_version", + "пакет_sdk", + "доменный_объект", + "публичный_метод_sdk", + "тип_запроса", + "тип_ответа", + "тип_теста", + "примечания", + ] + if headers != expected_headers: + raise ValueError(f"Неожиданные колонки inventory: {headers!r}") + + rows: list[InventoryRow] = [] + for line in table[2:]: + if line.startswith(""): + break + cells = parse_markdown_table(line) + if len(cells) != len(expected_headers): + raise ValueError(f"Некорректная строка operations table: {line}") + deprecated = parse_bool(cells[5]) + rows.append( + InventoryRow( + section=cells[0], + document=cells[1], + method=cells[2].upper(), + path=cells[3], + description=cells[4], + deprecated=deprecated, + deprecated_since=parse_optional(cells[6]), + replacement=parse_optional(cells[7]), + removal_version=parse_optional(cells[8]), + sdk_package=cells[9], + domain_object=cells[10], + sdk_public_method=cells[11], + request_type=cells[12], + response_type=cells[13], + test_type=cells[14], + notes=parse_optional(cells[15]), + ) + ) + return rows + + +def main() -> None: + parser = argparse.ArgumentParser(description="Разобрать docs/avito/inventory.md.") + parser.add_argument("--inventory", type=Path, default=DEFAULT_INVENTORY_PATH) + parser.add_argument("--documents", action="store_true") + args = parser.parse_args() + + rows = parse_documents(args.inventory) if args.documents else parse_inventory(args.inventory) + print(json.dumps([asdict(row) for row in rows], ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tests/contracts/test_deprecation_warnings.py b/tests/contracts/test_deprecation_warnings.py new file mode 100644 index 0000000..7cea0da --- /dev/null +++ b/tests/contracts/test_deprecation_warnings.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import warnings +from collections.abc import Callable + +import httpx +import pytest + +from avito.ads import AutoloadArchive +from avito.core.deprecation import _WARNED_SYMBOLS +from avito.cpa import CpaArchive, CpaChat +from scripts.parse_inventory import InventoryRow, parse_inventory +from tests.helpers.transport import make_transport + + +def response_for(path: str) -> httpx.Response: + if path == "/cpa/v1/call/101": + return httpx.Response(200, content=b"ID3", headers={"content-type": "audio/mpeg"}) + if path == "/cpa/v1/chatsByTime": + return httpx.Response(200, json={"chats": []}) + if path == "/cpa/v2/balanceInfo": + return httpx.Response(200, json={"balance": -5000, "advance": 1000, "debt": 0}) + if path == "/cpa/v2/callById": + return httpx.Response(200, json={"calls": {"id": 101}}) + if path == "/autoload/v1/profile": + return httpx.Response(200, json={"userId": 7, "isEnabled": True, "uploadUrl": "https://example.test/upload"}) + if path == "/autoload/v2/reports/last_completed_report": + return httpx.Response(200, json={"reportId": 11, "status": "completed"}) + if path == "/autoload/v2/reports/101": + return httpx.Response(200, json={"reportId": 101, "status": "completed"}) + raise AssertionError(f"Неожиданный маршрут теста deprecated: {path}") + + +def make_deprecated_transport() -> object: + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/autoload/v1/profile" and request.method == "POST": + return httpx.Response(200, json={"success": True}) + return response_for(request.url.path) + + return make_transport(httpx.MockTransport(handler)) + + +def deprecated_cases() -> list[tuple[InventoryRow, Callable[[], object]]]: + transport = make_deprecated_transport() + cpa_archive = CpaArchive(transport, call_id=101) + cpa_chat = CpaChat(transport) + autoload_archive = AutoloadArchive(transport, report_id=101) + calls: dict[tuple[str, str], Callable[[], object]] = { + ("cpa", "CpaArchive.get_call"): lambda: cpa_archive.get_call(), + ("cpa", "CpaChat.list"): lambda: cpa_chat.list( + created_at_from="2026-04-18T00:00:00+03:00", + version=1, + ), + ("cpa", "CpaArchive.get_balance_info"): lambda: cpa_archive.get_balance_info(), + ("cpa", "CpaArchive.get_call_by_id"): lambda: cpa_archive.get_call_by_id(call_id=101), + ("ads", "AutoloadArchive.get_profile"): lambda: autoload_archive.get_profile(), + ("ads", "AutoloadArchive.save_profile"): lambda: autoload_archive.save_profile(is_enabled=True), + ("ads", "AutoloadArchive.get_last_completed_report"): ( + lambda: autoload_archive.get_last_completed_report() + ), + ("ads", "AutoloadArchive.get_report"): lambda: autoload_archive.get_report(), + } + + cases: list[tuple[InventoryRow, Callable[[], object]]] = [] + for row in parse_inventory(): + if not row.deprecated: + continue + key = (row.sdk_package, f"{row.domain_object}.{row.sdk_public_method}") + if key not in calls: + raise AssertionError(f"Нет deprecated-test case для {key}") + cases.append((row, calls[key])) + return cases + + +@pytest.mark.parametrize(("row", "call"), deprecated_cases()) +def test_deprecated_inventory_symbols_warn_once( + row: InventoryRow, + call: Callable[[], object], +) -> None: + _WARNED_SYMBOLS.clear() + + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always", DeprecationWarning) + call() + call() + + deprecation_warnings = [ + warning for warning in recorded if issubclass(warning.category, DeprecationWarning) + ] + assert len(deprecation_warnings) == 1 + + message = str(deprecation_warnings[0].message) + assert row.replacement is not None + assert row.removal_version is not None + assert row.deprecated_since is not None + assert row.replacement in message + assert row.removal_version in message + assert row.deprecated_since in message diff --git a/tests/domains/cpa/test_cpa.py b/tests/domains/cpa/test_cpa.py index 83f94b2..485727b 100644 --- a/tests/domains/cpa/test_cpa.py +++ b/tests/domains/cpa/test_cpa.py @@ -3,6 +3,7 @@ import json import httpx +import pytest from avito.cpa import CallTrackingCall, CpaArchive, CpaCall, CpaChat, CpaLead from tests.helpers.transport import make_transport @@ -23,7 +24,9 @@ def handler(request: httpx.Request) -> httpx.Response: chat = CpaChat(make_transport(httpx.MockTransport(handler)), action_id="act-1") assert chat.get().item_title == "Велосипед" - assert chat.list(created_at_from="2026-04-18T00:00:00+03:00", version=1).items[0].buyer_name == "Петр" + with pytest.deprecated_call(match="cpa_chat\\(\\)\\.list\\(version=2\\)"): + classic_chats = chat.list(created_at_from="2026-04-18T00:00:00+03:00", version=1) + assert classic_chats.items[0].buyer_name == "Петр" assert chat.list(created_at_from="2026-04-18T00:00:00+03:00", limit=10).items[0].is_arbitrage_available is True assert chat.get_phones_info_from_chats(action_ids=["act-1", "act-2"]).items[1].phone_number == "+79990000002" @@ -56,9 +59,15 @@ def handler(request: httpx.Request) -> httpx.Response: assert cpa_call.create_complaint(call_id=2001, reason="spam").success is True assert cpa_lead.create_complaint_by_action_id(action_id="act-1", reason="duplicate").success is True assert cpa_lead.get_balance_info().balance == -5000 - assert archive.get_balance_info().advance == 1000 - assert archive.get_call_by_id(call_id=2001).call_id == "2001" - assert archive.get_call().binary.content == audio_bytes + with pytest.deprecated_call(match="cpa_lead\\(\\)\\.get_balance_info"): + archived_balance = archive.get_balance_info() + with pytest.deprecated_call(match="call_tracking_call\\(\\)\\.get"): + archived_call = archive.get_call_by_id(call_id=2001) + with pytest.deprecated_call(match="call_tracking_call\\(\\)\\.download"): + archived_audio = archive.get_call() + assert archived_balance.advance == 1000 + assert archived_call.call_id == "2001" + assert archived_audio.binary.content == audio_bytes def test_calltracking_flows() -> None: diff --git a/usability_scorecard.md b/usability_scorecard.md index 1c712b4..430b8b3 100644 --- a/usability_scorecard.md +++ b/usability_scorecard.md @@ -1,623 +1,623 @@ -# Методика оценки UX Python SDK +# Python SDK UX Evaluation Methodology -Воспроизводимая методика диагностики developer experience (DX) любого Python-SDK, работающего поверх HTTP API. Цель методики — не бинарная проверка, а поиск слабых мест пользовательского опыта и приоритезация улучшений. +A reproducible methodology for diagnosing the developer experience (DX) of any Python SDK built on top of an HTTP API. The goal is not a binary pass/fail check, but finding weak spots in the user experience and prioritizing improvements. -Методика универсальна: она не привязана к конкретным классам, файлам или методам. Все критерии формулируются через **роли**, **контракты** и **поведение**, а не через имена в коде. +The methodology is universal: it is not tied to specific classes, files, or method names. All criteria are formulated through **roles**, **contracts**, and **behavior**, not code identifiers. --- -## 1. Что оценивает методика +## 1. What the Methodology Evaluates -- **Удобство интеграции.** Путь от «установил пакет» до «сделал первый успешный запрос». -- **Удобство повседневной разработки.** Скорость поиска нужного метода, предсказуемость поведения, стоимость отладки, стоимость тестирования своего кода поверх SDK. -- **Удобство сопровождения.** Поведение при апгрейде, при смене API upstream, при смене окружения, при сбоях сети. +- **Integration experience.** The path from "installed the package" to "made the first successful request". +- **Day-to-day development experience.** Speed of finding the right method, predictability of behavior, cost of debugging, cost of testing one's own code on top of the SDK. +- **Maintenance experience.** Behavior on upgrade, on upstream API changes, on environment changes, on network failures. -Методика **не** оценивает: +The methodology does **not** evaluate: -- производительность (latency, throughput, память); -- архитектурную чистоту ради чистоты; -- полноту функциональности upstream API (это отдельная метрика «coverage», входящая только частично). +- performance (latency, throughput, memory); +- architectural cleanliness for its own sake; +- upstream API feature completeness (this is a separate "coverage" metric, only partially included). -## 2. Цели, ограничения и допущения +## 2. Goals, Constraints, and Assumptions -**Цели:** +**Goals:** -1. Получить количественную оценку `Score ∈ [0, 100]`, сопоставимую между релизами и между разными SDK. -2. Локализовать слабые места до уровня поведения/контракта (без привязки к конкретному файлу). -3. Выдать приоритезированный backlog улучшений (через индекс `Pain`, см. §6). +1. Produce a quantitative score `Score ∈ [0, 100]`, comparable across releases and across different SDKs. +2. Localize weak spots down to the behavior/contract level (without binding to a specific file). +3. Produce a prioritized improvement backlog (via the `Pain` index, see §6). -**Ограничения:** +**Constraints:** -- Методика рассчитана на SDK-обёртки поверх HTTP/REST API. Для gRPC, WebSocket или низкоуровневых библиотек часть критериев неприменима и отключается явно. -- Минимальная оценка — один оценщик за ~1 рабочий день. Эталонная — два независимых оценщика, кросс-проверка. -- Некоторые критерии требуют участия пользователей (персон-сессии); если доступа к реальным пользователям нет, применяется имитационный режим (оценщик играет роль персоны). +- The methodology is designed for SDK wrappers on top of HTTP/REST APIs. For gRPC, WebSocket, or low-level libraries, some criteria are not applicable and are explicitly disabled. +- Minimum evaluation: one evaluator in ~1 working day. Reference evaluation: two independent evaluators with cross-check. +- Some criteria require user participation (persona sessions); if access to real users is unavailable, a simulation mode is used (the evaluator plays the persona role). -**Допущения:** +**Assumptions:** -- SDK имеет публичный репозиторий или доступ к исходникам. -- SDK распространяется через PyPI/подобный индекс, устанавливается одним шагом. -- Есть хотя бы минимальная документация (README, docstring). +- The SDK has a public repository or source code access. +- The SDK is distributed via PyPI/similar index, installable in one step. +- At least minimal documentation exists (README, docstrings). --- -## 3. Персоны +## 3. Personas -Каждый подкритерий оценивается с позиции трёх персон. Итоговый балл подпункта = **минимум** из трёх оценок (слабое звено определяет DX). +Each sub-criterion is evaluated from the perspective of three personas. The final score for a sub-criterion = **minimum** of the three scores (the weakest link determines DX). -- **P1. Интегратор-новичок.** Впервые использует SDK. Опирается на README, quickstart и IDE-подсказки. Читает исходный код только в крайнем случае. -- **P2. Опытный разработчик.** Пишет продакшен-код, использует `mypy`, отлаживает инциденты, пишет свои тесты поверх SDK. -- **P3. Сопровождающий.** Обновляет версию SDK в своём проекте, следит за релизами, поддерживает обратную совместимость собственного кода, собирает баг-репорты. +- **P1. Newcomer integrator.** Using the SDK for the first time. Relies on README, quickstart, and IDE hints. Reads source code only as a last resort. +- **P2. Experienced developer.** Writes production code, uses `mypy`, debugs incidents, writes their own tests on top of the SDK. +- **P3. Maintainer.** Updates the SDK version in their project, monitors releases, maintains backward compatibility of their own code, collects bug reports. --- -## 4. Методы сбора данных +## 4. Data Collection Methods -Единый набор методов — в каждом критерии явно указано, какие применяются. +A single set of methods — each criterion explicitly states which apply. -| Код | Метод | Описание | +| Code | Method | Description | |---|---|---| -| **AC** | Auditing Code (аудит кода) | Ручное чтение публичного API, не углубляясь в реализацию. Роль оценщика — «умный читатель», а не автор | -| **SA** | Static Analysis | Применение статических анализаторов: типов, стиля, docstring, мёртвого кода, security-lint | -| **FI** | Fault Injection | Контролируемая подстановка ошибок (5xx, 401, timeout, битый JSON) через моки и прокси | -| **TT** | Timed Task | Задача на время: замеряется, за сколько персона доходит до результата | -| **TAL** | Think-Aloud Lab | Пользовательская сессия с протоколом «думаю вслух»: фиксация моментов застревания | -| **HE** | Heuristic Evaluation | Эвристический анализ по 10 DX-эвристикам (см. §5.5) | -| **CB** | Comparative Benchmark | Сравнение конкретного поведения с эталонными SDK (Stripe, Azure, Google Cloud, boto3, OpenAI) | -| **DA** | Document Audit | Сверка публичной документации с фактическим поведением SDK | -| **LA** | Log Analysis | Анализ логов SDK: что пишется, на каких уровнях, нет ли утечек секретов | -| **AM** | Automated Metric | Любой машинно-извлекаемый показатель: покрытие тестами, количество публичных символов с docstring, число `TODO`/`FIXME` и т. д. | - -Каждый подкритерий должен иметь **≥1 метод из множества {SA, FI, DA, AM, CB}** — то есть хотя бы один источник, не зависящий от субъективного суждения. Ручные методы (AC, TT, TAL, HE) применяются в дополнение, но не как единственный источник. +| **AC** | Auditing Code | Manual reading of the public API without diving into implementation. The evaluator's role is "smart reader", not author | +| **SA** | Static Analysis | Applying static analyzers: types, style, docstrings, dead code, security lint | +| **FI** | Fault Injection | Controlled error injection (5xx, 401, timeout, malformed JSON) via mocks and proxies | +| **TT** | Timed Task | Time-based task: measuring how long it takes a persona to reach the result | +| **TAL** | Think-Aloud Lab | User session with a "think aloud" protocol: recording moments of getting stuck | +| **HE** | Heuristic Evaluation | Heuristic analysis using 10 DX heuristics (see §5.5) | +| **CB** | Comparative Benchmark | Comparing specific behavior against reference SDKs (Stripe, Azure, Google Cloud, boto3, OpenAI) | +| **DA** | Document Audit | Verifying public documentation against actual SDK behavior | +| **LA** | Log Analysis | Analyzing SDK logs: what is written, at what levels, whether secrets leak | +| **AM** | Automated Metric | Any machine-extractable metric: test coverage, number of public symbols with docstrings, number of `TODO`/`FIXME`, etc. | + +Each sub-criterion must have **≥1 method from {SA, FI, DA, AM, CB}** — at least one source independent of subjective judgment. Manual methods (AC, TT, TAL, HE) are applied in addition, but not as the sole source. --- -## 5. Инструменты +## 5. Tools -### 5.1 Статический анализ Python +### 5.1 Python Static Analysis -- **ruff** — стилистические нарушения, базовые антипаттерны, dead code, часть `flake8`-правил. -- **mypy** в режиме `--strict` — полнота типизации, `Any`-утечки, расхождения аннотаций и runtime. -- **pyright** (альтернативно) — более быстрый и строгий статический анализатор типов. -- **bandit** — security-lint (hardcoded secrets, небезопасные паттерны). -- **vulture** — поиск мёртвого кода: неиспользуемых импортов, функций, классов. -- **radon** / **xenon** — цикломатическая сложность публичных методов. -- **interrogate** — покрытие публичного API docstring'ами. -- **pydocstyle** — формат docstring. +- **ruff** — style violations, basic anti-patterns, dead code, subset of `flake8` rules. +- **mypy** in `--strict` mode — completeness of type annotations, `Any` leaks, annotation/runtime mismatches. +- **pyright** (alternative) — faster and stricter static type analyzer. +- **bandit** — security lint (hardcoded secrets, unsafe patterns). +- **vulture** — dead code detection: unused imports, functions, classes. +- **radon** / **xenon** — cyclomatic complexity of public methods. +- **interrogate** — docstring coverage of public API. +- **pydocstyle** — docstring format. -### 5.2 Динамические тесты и Fault Injection +### 5.2 Dynamic Tests and Fault Injection -- **pytest** — база для контрактных, интеграционных, error-mapping тестов. -- **respx** / **httpx-mock** — мок HTTP-слоя для `httpx`-клиентов. -- **vcrpy** / **pytest-recording** — запись и воспроизведение реальных HTTP-сессий для регрессии. -- **pytest-httpserver** — поднятие тестового HTTP-сервера, способного имитировать 4xx/5xx/medleys. -- **toxiproxy** / **mitmproxy** — имитация сетевых аномалий: jitter, обрывы, rate-limit. -- **hypothesis** — property-based тесты для граничных условий сериализации и мапперов. -- **freezegun** — фиксация времени для проверки retry-after и backoff. +- **pytest** — base for contract, integration, and error-mapping tests. +- **respx** / **httpx-mock** — HTTP layer mock for `httpx`-based clients. +- **vcrpy** / **pytest-recording** — recording and replaying real HTTP sessions for regression. +- **pytest-httpserver** — spin up a test HTTP server capable of simulating 4xx/5xx/medleys. +- **toxiproxy** / **mitmproxy** — simulating network anomalies: jitter, drops, rate limiting. +- **hypothesis** — property-based tests for edge cases in serialization and mappers. +- **freezegun** — time freezing for checking retry-after and backoff behavior. -### 5.3 Инструменты измерения документации +### 5.3 Documentation Measurement Tools -- **vale** — прозовый linter для стиля документации и терминологии. -- **markdownlint** — качество разметки Markdown. -- **sphinx-build -W** — сборка reference без предупреждений. -- **link-checker** (например, `lychee`) — проверка мёртвых ссылок. -- **Diátaxis-матрица** — ручная категоризация существующих материалов (см. §9.15). +- **vale** — prose linter for documentation style and terminology. +- **markdownlint** — Markdown quality. +- **sphinx-build -W** — reference build without warnings. +- **link-checker** (e.g., `lychee`) — dead link detection. +- **Diátaxis matrix** — manual categorization of existing materials (see §9.15). -### 5.4 Инструменты UX-исследования +### 5.4 UX Research Tools -- **Time-to-First-Call (TTFC)** — метрика в секундах от `pip install` до первого успешного реального запроса, замеряется секундомером. -- **Task Success Rate (TSR)** — доля пользователей, решивших тестовую задачу без помощи. -- **System Usability Scale (SUS)** в варианте **DevSUS** — анкета из 10 вопросов, адаптированная для SDK. Итог от 0 до 100, эталон ≥80. -- **Think-aloud protocol** — пользователь комментирует ход решения; оценщик фиксирует точки застревания. -- **Diary study** (для P3) — сбор заметок сопровождающего за 1–2 спринта использования. +- **Time-to-First-Call (TTFC)** — metric in seconds from `pip install` to first successful real request, measured with a stopwatch. +- **Task Success Rate (TSR)** — share of users who solved the test task without assistance. +- **System Usability Scale (SUS)** in **DevSUS** variant — 10-question questionnaire adapted for SDKs. Result from 0 to 100, reference threshold ≥80. +- **Think-aloud protocol** — user narrates their progress; evaluator records points of getting stuck. +- **Diary study** (for P3) — collecting a maintainer's notes over 1–2 sprints of use. -### 5.5 Эвристики DX Python SDK (HE) +### 5.5 DX Heuristics for Python SDK (HE) -Адаптированный чек-лист Нильсена для SDK: +Adapted Nielsen checklist for SDKs: -1. **Видимость состояния.** Пользователь понимает, где сейчас происходит сетевой вызов, а где нет. -2. **Соответствие доменному языку.** Имена методов отражают операции предметной области, не HTTP. -3. **Свобода и контроль.** Есть per-operation overrides и «побег» в низкоуровневые примитивы при необходимости. -4. **Консистентность и стандарты.** SDK следует Python-конвенциям (snake_case, PEP 8) и внутренним конвенциям по всей поверхности. -5. **Предупреждение ошибок.** Некорректный ввод ловится до сети (fail-fast). -6. **Узнавание, а не вспоминание.** IDE-autocomplete предоставляет всё необходимое без обращения к документации. -7. **Гибкость и эффективность.** Новичок проходит happy-path за минуты; опытный пользователь получает доступ к тонкой настройке. -8. **Минимализм.** Публичный API содержит только то, что нужно; остальное скрыто. -9. **Язык ошибок.** Сообщения помогают диагностировать и действовать, не просто констатируют факт. -10. **Справка и документация.** Покрыты все четыре режима Diátaxis: tutorials, how-to, reference, explanations. +1. **Visibility of state.** The user understands where a network call happens and where it does not. +2. **Match to domain language.** Method names reflect domain operations, not HTTP. +3. **Freedom and control.** Per-operation overrides exist, and there is an "escape hatch" to low-level primitives when needed. +4. **Consistency and standards.** The SDK follows Python conventions (snake_case, PEP 8) and internal conventions throughout the surface. +5. **Error prevention.** Incorrect input is caught before the network call (fail-fast). +6. **Recognition over recall.** IDE autocomplete provides everything needed without consulting documentation. +7. **Flexibility and efficiency.** A newcomer completes the happy path in minutes; an experienced user has access to fine-tuning. +8. **Minimalism.** The public API contains only what is needed; everything else is hidden. +9. **Language of errors.** Messages help diagnose and act, not merely state a fact. +10. **Help and documentation.** All four Diátaxis modes are covered: tutorials, how-to, reference, explanations. -### 5.6 Эталоны для Comparative Benchmark (CB) +### 5.6 Reference SDKs for Comparative Benchmark (CB) -Сравнение поведения проводится с эталонными Python-SDK: +Behavior comparisons are made against reference Python SDKs: -- **Stripe** (`stripe-python`) — идемпотентность, auto-pagination, error-mapping. -- **Azure SDK for Python** — клиент-конструктор, long-running operations, `ItemPaged`. -- **Google Cloud Python** — ADC, прозрачная пагинация. -- **boto3** — конфигурация, ретраи, сессии. -- **OpenAI Python SDK** — async/sync parity, потоковые ответы. +- **Stripe** (`stripe-python`) — idempotency, auto-pagination, error mapping. +- **Azure SDK for Python** — client constructor, long-running operations, `ItemPaged`. +- **Google Cloud Python** — ADC, transparent pagination. +- **boto3** — configuration, retries, sessions. +- **OpenAI Python SDK** — async/sync parity, streaming responses. -Для каждого поведения методика требует сформулировать ответ: «в эталоне реализовано через X, в оцениваемом SDK реализовано через Y; разница в DX такая-то». +For each behavior, the methodology requires formulating an answer: "in the reference it is implemented via X, in the evaluated SDK via Y; the DX difference is Z". --- -## 6. Шкала, формула и приоритизация +## 6. Scale, Formula, and Prioritization -### 6.1 Шкала оценки подкритерия +### 6.1 Sub-criterion Scoring Scale -| Уровень | Балл | Признак | +| Level | Score | Indicator | |---|---|---| -| Отсутствует | 0% | Требование не выполнено; дефект виден в первые минуты работы | -| Частичное | 25% | Выполнено в отдельных местах, в остальных нарушено | -| Базовое | 50% | Выполнено для большинства случаев, есть системные исключения | -| Хорошее | 75% | Выполнено везде, редкие мелкие отклонения | -| Эталонное | 100% | Выполнено, нет признаков слабости, сопоставимо с лучшими SDK | +| Absent | 0% | Requirement not met; defect visible in the first minutes | +| Partial | 25% | Met in some places, violated elsewhere | +| Basic | 50% | Met in most cases, with systematic exceptions | +| Good | 75% | Met everywhere, rare minor deviations | +| Reference | 100% | Met, no signs of weakness, comparable to best SDKs | -Промежуточные значения допустимы с обоснованием. +Intermediate values are allowed with justification. -### 6.2 Формула интегральной оценки +### 6.2 Integral Score Formula ``` Score = Σ ( weight_criterion_i × Σ ( weight_subcriterion_ij × grade_ij ) ) ``` -где `grade_ij ∈ [0, 1]`, `Σ weight_criterion = 100%`, внутри каждого критерия `Σ weight_subcriterion = weight_criterion`. +where `grade_ij ∈ [0, 1]`, `Σ weight_criterion = 100%`, and within each criterion `Σ weight_subcriterion = weight_criterion`. -### 6.3 Интерпретация +### 6.3 Interpretation -- **≥ 90%** — эталонный DX, сопоставимо с лучшими SDK индустрии. -- **75–89%** — рабочий инструмент, заметные шероховатости в 1–2 критериях. -- **60–74%** — DX-долг, нужен плановый рефакторинг. -- **< 60%** — требуется архитектурный пересмотр до широкой публикации. +- **≥ 90%** — reference DX, comparable to industry-leading SDKs. +- **75–89%** — working tool, noticeable rough edges in 1–2 criteria. +- **60–74%** — DX debt, planned refactoring needed. +- **< 60%** — architectural revision required before wide publication. -### 6.4 Индекс боли (Pain-ranking) +### 6.4 Pain Index (Pain-ranking) -Для каждого подкритерия с `grade < 0.75`: +For each sub-criterion with `grade < 0.75`: ``` Pain = weight_subcriterion × (1 - grade) × persona_multiplier × cadence_multiplier ``` -где: +where: -- `persona_multiplier = 1.5`, если просадка затрагивает персону P1 (новичок), иначе 1.0 — новичок уходит из SDK быстрее всего; -- `cadence_multiplier = 1.3`, если слабость проявляется в повседневном сценарии (пишется/читается в каждой второй строке кода пользователя), 1.0 в остальных случаях. +- `persona_multiplier = 1.5` if the deficiency affects persona P1 (newcomer), otherwise 1.0 — newcomers leave an SDK fastest; +- `cadence_multiplier = 1.3` if the weakness manifests in everyday scenarios (written/read in every other line of user code), 1.0 otherwise. -Backlog сортируется по убыванию `Pain`. Верхний квартиль — цель ближайшего спринта. +The backlog is sorted by descending `Pain`. The top quartile is the target for the nearest sprint. --- -## 7. Критерии +## 7. Criteria -Сумма весов = 100%. +Sum of weights = 100%. -| # | Критерий | Вес | +| # | Criterion | Weight | |---|---|---| -| 1 | Онбординг и Time-to-First-Call | 10% | -| 2 | Discoverability публичного API | 8% | -| 3 | Именование и согласованность | 4% | -| 4 | Типизация и IDE-friendliness | 10% | -| 5 | Моделирование данных и возвращаемые типы | 6% | -| 6 | Обработка ошибок и actionability | 8% | -| 7 | Безопасность использования | 6% | -| 8 | Надёжность под нестабильной сетью | 7% | -| 9 | Идемпотентность write-операций | 3% | -| 10 | Пагинация и работа с коллекциями | 4% | -| 11 | Per-operation overrides | 3% | -| 12 | Async/sync parity | 3% | -| 13 | Конфигурация и fail-fast | 4% | -| 14 | Покрытие контракта API | 4% | -| 15 | Документация (Diátaxis) | 7% | -| 16 | Тестируемость пользовательским кодом | 5% | -| 17 | Observability и логирование | 3% | -| 18 | Совместимость и deprecation policy | 5% | - -Ниже — развёрнутое описание каждого критерия. +| 1 | Onboarding and Time-to-First-Call | 10% | +| 2 | Public API Discoverability | 8% | +| 3 | Naming and Consistency | 4% | +| 4 | Typing and IDE-friendliness | 10% | +| 5 | Data Modeling and Return Types | 6% | +| 6 | Error Handling and Actionability | 8% | +| 7 | Safety of Use | 6% | +| 8 | Reliability Under Unstable Network | 7% | +| 9 | Idempotency of Write Operations | 3% | +| 10 | Pagination and Collection Handling | 4% | +| 11 | Per-operation Overrides | 3% | +| 12 | Async/Sync Parity | 3% | +| 13 | Configuration and Fail-fast | 4% | +| 14 | API Contract Coverage | 4% | +| 15 | Documentation (Diátaxis) | 7% | +| 16 | Testability by User Code | 5% | +| 17 | Observability and Logging | 3% | +| 18 | Compatibility and Deprecation Policy | 5% | + +Detailed descriptions of each criterion follow. --- -### 8. Критерий 1. Онбординг и Time-to-First-Call — 10% +### 8. Criterion 1. Onboarding and Time-to-First-Call — 10% -**Цель.** Измерить путь от «узнал про SDK» до «увидел первый результат». +**Goal.** Measure the path from "learned about the SDK" to "saw the first result". -**Ключевая метрика.** TTFC (минуты). +**Key metric.** TTFC (minutes). -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 1.1 | Установка одной командой без системных зависимостей | 1% | TT, DA | `pip install` в чистом venv, замер времени и ошибок | -| 1.2 | Quickstart в README воспроизводим copy-paste | 2% | TT, DA | Чистый venv, прогон примеров из README, подсчёт ручных правок | -| 1.3 | Первый успешный вызов требует ≤5 строк кода | 2% | AC, TT | Подсчёт LoC для канонического сценария | -| 1.4 | Переменные окружения документированы, priority-resolution описан | 1% | DA | Сверка документации с поведением при конфликте `env` vs `.env` | -| 1.5 | Fail-fast на некорректной конфигурации | 2% | FI | Удаление обязательных полей, проверка: ошибка до сети, понятный класс, без секретов | -| 1.6 | Canonical TTFC ≤ 15 минут для P1 | 2% | TT, TAL | Видеозапись сессии новичка, секундомер | +| 1.1 | Installation with one command, no system dependencies | 1% | TT, DA | `pip install` in a clean venv, measure time and errors | +| 1.2 | README quickstart reproducible via copy-paste | 2% | TT, DA | Clean venv, run README examples, count manual edits | +| 1.3 | First successful call requires ≤5 lines of code | 2% | AC, TT | LoC count for the canonical scenario | +| 1.4 | Environment variables are documented, priority resolution is described | 1% | DA | Verify documentation against behavior when `env` vs `.env` conflict | +| 1.5 | Fail-fast on invalid configuration | 2% | FI | Remove required fields, verify: error before network, clear exception class, no secrets | +| 1.6 | Canonical TTFC ≤ 15 minutes for P1 | 2% | TT, TAL | Video recording of newcomer session, stopwatch | -**Процедура.** +**Procedure.** -1. Поднять чистый venv, зафиксировать точное время. -2. Выполнить `pip install `; засечь время установки и вывод предупреждений. -3. Скопировать quickstart в файл, запустить. Любая ручная правка уменьшает балл 1.2 на 25%. -4. Замерить TTFC до первого реального сетевого ответа. -5. Намеренно убрать одно обязательное поле конфига, поймать исключение, оценить качество сообщения. +1. Spin up a clean venv, record exact time. +2. Run `pip install `; record installation time and warnings. +3. Copy quickstart to a file, run it. Each manual edit reduces score 1.2 by 25%. +4. Measure TTFC until the first real network response. +5. Intentionally remove one required config field, catch the exception, evaluate the message quality. -**Симптомы слабых мест.** +**Symptoms of weak spots.** -- README ведёт через «полный пример» раньше «минимального». -- Обязательные поля требуют создания промежуточных объектов. -- Ошибка конфигурации маскируется под сетевое исключение. -- `pip install` требует системных пакетов без предупреждения. +- README leads through a "full example" before a "minimal example". +- Required fields require creating intermediate objects. +- Configuration error masquerades as a network exception. +- `pip install` requires system packages without warning. -**Шаблон фиксации:** «Канонический сценарий — N строк, TTFC — M минут, ручных правок — K, первое сообщение ошибки конфигурации — `<цитата>`». +**Recording template:** "Canonical scenario — N lines, TTFC — M minutes, manual edits — K, first configuration error message — ``". --- -### 9. Критерий 2. Discoverability публичного API — 8% +### 9. Criterion 2. Public API Discoverability — 8% -**Цель.** Измерить, сколько усилий нужно, чтобы найти правильный метод без чтения исходников. +**Goal.** Measure how much effort is needed to find the right method without reading source code. -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 2.1 | Единая точка входа (фасад) | 2% | AC, HE-2, CB | Сравнение с Stripe/Azure: все сценарии от одного корня | -| 2.2 | Фабричные методы по предметным областям | 2% | AC | IDE autocomplete на корневом клиенте показывает все домены | -| 2.3 | Один очевидный путь для каждой операции | 2% | AC, DA | Поиск дублей по документации и по публичным именам | -| 2.4 | Навигация по структуре пакета предсказуема | 1% | AC, HE-4 | Соответствие имен подпакетов разделам API | -| 2.5 | Имена методов отражают бизнес-действия | 1% | AC, HE-2 | Отсутствие `post_*`/`do_*` стилей | +| 2.1 | Single entry point (facade) | 2% | AC, HE-2, CB | Comparison with Stripe/Azure: all scenarios from one root | +| 2.2 | Factory methods by domain area | 2% | AC | IDE autocomplete on the root client shows all domains | +| 2.3 | One obvious path for each operation | 2% | AC, DA | Search for duplicates in documentation and public names | +| 2.4 | Package structure navigation is predictable | 1% | AC, HE-4 | Sub-package names match API sections | +| 2.5 | Method names reflect business actions | 1% | AC, HE-2 | Absence of `post_*`/`do_*` style names | -**Процедура.** +**Procedure.** -1. Дать P1 задачу сформулированную на языке предметной области (напр. «отправь сообщение X пользователю Y»). Замерить время до правильного метода. -2. Открыть корневой объект SDK в IDE, проверить наличие всех доменов в autocomplete. -3. Собрать список публичных методов и проверить дубли: две точки доступа к одним данным — минус балл 2.3. +1. Give P1 a task formulated in domain language (e.g., "send message X to user Y"). Measure time to the correct method. +2. Open the root SDK object in IDE, verify all domains appear in autocomplete. +3. Collect list of public methods and check for duplicates: two access points to the same data — deduct score 2.3. -**Симптомы.** +**Symptoms.** -- Для одного действия есть два эквивалентных пути без пометки deprecation. -- Имена подпакетов не соответствуют привычным названиям операций. -- `find . -name "utils*.py"` находит файлы с существенной бизнес-логикой. +- Two equivalent paths exist for one action without a deprecation marker. +- Sub-package names don't match familiar operation names. +- `find . -name "utils*.py"` finds files containing significant business logic. --- -### 10. Критерий 3. Именование и согласованность — 4% +### 10. Criterion 3. Naming and Consistency — 4% -**Цель.** Согласованный стиль именования = меньше времени на изучение. +**Goal.** Consistent naming style = less time to learn. -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 3.1 | Следование PEP 8 и Python-конвенциям | 1% | SA | `ruff`, `pep8-naming` | -| 3.2 | Согласованные префиксы операций (`get_`, `list_`, `create_`, ...) | 1% | AC, AM | Скрипт, группирующий публичные методы по префиксу | -| 3.3 | Идентификаторы доменно-специфичные (без `resource_id`, `entity_id`) | 1% | AM | grep по абстрактным именам в публичных сигнатурах | -| 3.4 | Одинаковый порядок параметров в похожих методах | 1% | AC | Визуальное сравнение сигнатур одного семейства | +| 3.1 | Follows PEP 8 and Python conventions | 1% | SA | `ruff`, `pep8-naming` | +| 3.2 | Consistent operation prefixes (`get_`, `list_`, `create_`, ...) | 1% | AC, AM | Script grouping public methods by prefix | +| 3.3 | Domain-specific identifiers (no `resource_id`, `entity_id`) | 1% | AM | grep for abstract names in public signatures | +| 3.4 | Consistent parameter order in similar methods | 1% | AC | Visual comparison of signatures in the same family | -**Процедура.** Прогнать `ruff` и `pep8-naming`; выгрузить список публичных методов в CSV с колонками `prefix`, `nouns`, `params`; визуально проанализировать. +**Procedure.** Run `ruff` and `pep8-naming`; export list of public methods to CSV with columns `prefix`, `nouns`, `params`; analyze visually. --- -### 11. Критерий 4. Типизация и IDE-friendliness — 10% +### 11. Criterion 4. Typing and IDE-friendliness — 10% -**Цель.** Защитить пользователя на уровне статики; дать максимум подсказок в IDE. +**Goal.** Protect the user at the static layer; provide maximum hints in the IDE. -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 4.1 | Публичный API полностью типизирован | 3% | SA | `mypy --strict`, `pyright --strict` | -| 4.2 | Нет `Any`/`dict[str, Any]`/`object` в публичных сигнатурах | 2% | SA, AM | grep по сигнатурам + mypy-репорт | -| 4.3 | Runtime-тип совпадает с аннотацией | 1% | AC, FI | Сравнение declared vs actual на коллекциях (итераторы, lazy) | -| 4.4 | Закрытые наборы значений выражены enum'ами | 2% | AC, DA | Сверка с OpenAPI: поля с `enum` должны быть типизированы | -| 4.5 | IDE показывает типы аргументов, return, исключений | 2% | HE-6, AC | Открыть 10 случайных публичных методов в IDE | +| 4.1 | Public API is fully typed | 3% | SA | `mypy --strict`, `pyright --strict` | +| 4.2 | No `Any`/`dict[str, Any]`/`object` in public signatures | 2% | SA, AM | grep signatures + mypy report | +| 4.3 | Runtime type matches annotation | 1% | AC, FI | Compare declared vs actual on collections (iterators, lazy) | +| 4.4 | Closed value sets expressed as enums | 2% | AC, DA | Cross-check with OpenAPI: fields with `enum` must be typed | +| 4.5 | IDE shows argument types, return types, and exceptions | 2% | HE-6, AC | Open 10 random public methods in IDE | -**Процедура.** CI-прогон `mypy --strict` и `pyright --strict` с выгрузкой списка ошибок; grep `": Any"`, `-> Any`, `-> dict`; выборка из 10 методов — ручная проверка IDE hover. +**Procedure.** Run `mypy --strict` and `pyright --strict` in CI, export error list; grep `": Any"`, `-> Any`, `-> dict`; sample 10 methods — manual IDE hover check. -**Инструменты ранжирования эффекта:** подкритерий 4.1 — это `exit_code` от mypy; 4.2 — количество попаданий grep делённое на общее число публичных сигнатур; 4.5 — оценочно, через HE. +**Effect ranking tools:** sub-criterion 4.1 — `exit_code` from mypy; 4.2 — grep hit count divided by total public signatures; 4.5 — assessed via HE. --- -### 12. Критерий 5. Моделирование данных и возвращаемые типы — 6% +### 12. Criterion 5. Data Modeling and Return Types — 6% -**Цель.** Пользователь работает с доменными объектами, а не с «сырыми» структурами. +**Goal.** Users work with domain objects, not "raw" structures. -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 5.1 | Публичные методы возвращают типизированные модели, не словари | 2% | AC, SA | -| 5.2 | Поля моделей имеют конкретные имена предметной области | 1% | AC | -| 5.3 | `required` и `Optional` выражены явно и совпадают со спекой | 1% | DA | -| 5.4 | Единая сериализация (`to_dict`/`model_dump`) для всех публичных моделей | 1% | AC, AM | -| 5.5 | Транспортные поля скрыты от публичных моделей | 1% | AC | +| 5.1 | Public methods return typed models, not dictionaries | 2% | AC, SA | +| 5.2 | Model fields have concrete domain names | 1% | AC | +| 5.3 | `required` and `Optional` are expressed explicitly and match the spec | 1% | DA | +| 5.4 | Uniform serialization (`to_dict`/`model_dump`) for all public models | 1% | AC, AM | +| 5.5 | Transport fields are hidden from public models | 1% | AC | -**Процедура.** Выборка 20 публичных моделей; для каждой: сериализовать в JSON стандартной функцией, проверить отсутствие служебных полей, сверить nullability со спецификацией. +**Procedure.** Sample 20 public models; for each: serialize to JSON using a standard function, verify absence of internal fields, cross-check nullability against the specification. --- -### 13. Критерий 6. Обработка ошибок и actionability — 8% +### 13. Criterion 6. Error Handling and Actionability — 8% -**Цель.** Ошибка помогает диагностировать и действовать. +**Goal.** Errors help diagnose and act. -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 6.1 | Иерархия доменных исключений покрывает значимые HTTP-статусы | 2% | FI | `pytest-httpserver` с последовательностью 400/401/403/404/409/422/429/5xx | -| 6.2 | Семантически различные ошибки не связаны наследованием | 1% | AC | Анализ графа исключений | -| 6.3 | Сообщения ошибок содержат operation, status, request-id, attempt | 2% | FI, AC | Захват исключения, проверка атрибутов | -| 6.4 | Сообщения actionable (что случилось + что делать) | 1% | HE-9 | Выборка 20 ошибок, оценка по чеклисту actionability | -| 6.5 | Parse-ошибки и transport-ошибки различимы по типу | 1% | FI | Подача битого JSON vs обрыва сети | -| 6.6 | Probe-методы возвращают `bool`, не бросают | 1% | FI, AC | Проверка поведения `exists`-style на 404 | +| 6.1 | Domain exception hierarchy covers significant HTTP statuses | 2% | FI | `pytest-httpserver` with sequence 400/401/403/404/409/422/429/5xx | +| 6.2 | Semantically distinct errors are not related by inheritance | 1% | AC | Exception graph analysis | +| 6.3 | Error messages contain operation, status, request-id, attempt | 2% | FI, AC | Capture exception, check attributes | +| 6.4 | Messages are actionable (what happened + what to do) | 1% | HE-9 | Sample 20 errors, evaluate against actionability checklist | +| 6.5 | Parse errors and transport errors are distinguishable by type | 1% | FI | Submit malformed JSON vs network drop | +| 6.6 | Probe methods return `bool`, don't throw | 1% | FI, AC | Check `exists`-style behavior on 404 | -**Процедура.** Построить матрицу `(статус × тип сбоя) → ожидаемый класс исключения`. Подать каждый случай через `pytest-httpserver` или `respx`. Зафиксировать несоответствия. Для 6.4 применить чек-лист actionability: (а) что случилось, (б) где, (в) что делать, (г) куда обратиться. +**Procedure.** Build a matrix `(status × failure type) → expected exception class`. Submit each case through `pytest-httpserver` or `respx`. Record mismatches. For 6.4, apply the actionability checklist: (a) what happened, (b) where, (c) what to do, (d) where to get help. --- -### 14. Критерий 7. Безопасность использования — 6% +### 14. Criterion 7. Safety of Use — 6% -**Цель.** Пользователь не должен утечь секреты через SDK. +**Goal.** Users must not leak secrets through the SDK. -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 7.1 | Секреты не появляются в логах любого уровня | 2% | LA | Перехват `logging`, регекс-проверка по маркерам `Bearer`, `client_secret=`, `Authorization` | -| 7.2 | Секреты не появляются в сообщениях исключений | 1% | FI | Провокация ошибки с заведомо secret-значением в параметре | -| 7.3 | Диагностический снапшот клиента безопасен по умолчанию | 1% | AC, FI | Вызов «диагностической» функции, проверка содержимого | -| 7.4 | Сериализация публичных моделей не раскрывает внутренние поля | 1% | AC | `json.dumps(model.to_dict())` по выборке моделей | -| 7.5 | `bandit` не выдаёт high-severity findings в публичных модулях | 1% | SA | `bandit -r ` | +| 7.1 | Secrets do not appear in logs at any level | 2% | LA | Intercept `logging`, regex check against markers `Bearer`, `client_secret=`, `Authorization` | +| 7.2 | Secrets do not appear in exception messages | 1% | FI | Provoke an error with a known secret value in a parameter | +| 7.3 | Client diagnostic snapshot is safe by default | 1% | AC, FI | Call a "diagnostic" function, check content | +| 7.4 | Serialization of public models does not expose internal fields | 1% | AC | `json.dumps(model.to_dict())` on a model sample | +| 7.5 | `bandit` reports no high-severity findings in public modules | 1% | SA | `bandit -r ` | -**Процедура.** Настроить тест, перехватывающий `logging` на уровне `DEBUG`, прогнать 10 типовых сценариев, прогрепать вывод по регексам секретов. Для 7.5 — CI-интеграция `bandit`. +**Procedure.** Set up a test that intercepts `logging` at `DEBUG` level, run 10 typical scenarios, grep output against secret regexes. For 7.5 — CI integration of `bandit`. --- -### 15. Критерий 8. Надёжность под нестабильной сетью — 7% +### 15. Criterion 8. Reliability Under Unstable Network — 7% -**Цель.** SDK переживает сетевые сбои без участия пользователя. +**Goal.** The SDK survives network failures without user intervention. -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 8.1 | Ретраи только на безопасных сценариях | 2% | FI | Последовательности `[500, 500, 200]`, `[429, 200]`, `[timeout, 200]`, `[POST, 500]` | -| 8.2 | Backoff экспоненциальный с jitter | 1% | FI, AM | `freezegun` + замер задержек между попытками, проверка дисперсии | -| 8.3 | `Retry-After` соблюдается на `429` | 1% | FI | Сервер возвращает `429 Retry-After: 3`, проверка реальной задержки | -| 8.4 | После исчерпания ретраев — доменное исключение | 1% | FI | `[500, 500, 500, 500]` → не `httpx.*`, а доменный тип | -| 8.5 | Таймауты connect/read/write настроены явно | 1% | AC | Чтение конструктора транспорта | -| 8.6 | Конфигурируемость политики retry/timeout | 1% | AC, CB | Сравнение с `stripe-python` и Azure SDK | +| 8.1 | Retries only on safe scenarios | 2% | FI | Sequences `[500, 500, 200]`, `[429, 200]`, `[timeout, 200]`, `[POST, 500]` | +| 8.2 | Exponential backoff with jitter | 1% | FI, AM | `freezegun` + measure delays between attempts, check variance | +| 8.3 | `Retry-After` is respected on `429` | 1% | FI | Server returns `429 Retry-After: 3`, check actual delay | +| 8.4 | After retries exhausted — domain exception, not transport exception | 1% | FI | `[500, 500, 500, 500]` → not `httpx.*`, but a domain type | +| 8.5 | Connect/read/write timeouts are configured explicitly | 1% | AC | Read transport constructor | +| 8.6 | Retry/timeout policy is configurable | 1% | AC, CB | Compare with `stripe-python` and Azure SDK | -**Процедура.** Подключить `toxiproxy` или mock-сервер с программируемой задержкой; для 8.2 — ≥5 попыток, измерить `stdev(delays) > 0`; для 8.3 — сравнить фактическую задержку с `Retry-After ± 20%`. +**Procedure.** Connect `toxiproxy` or a mock server with programmable delay; for 8.2 — ≥5 attempts, measure `stdev(delays) > 0`; for 8.3 — compare actual delay with `Retry-After ± 20%`. --- -### 16. Критерий 9. Идемпотентность write-операций — 3% +### 16. Criterion 9. Idempotency of Write Operations — 3% -**Цель.** Безопасный повтор write-вызова не создаёт дублей. +**Goal.** A safe repeat of a write call does not create duplicates. -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 9.1 | Публичные write-методы принимают `idempotency_key` | 1% | AC, CB (эталон — Stripe) | -| 9.2 | При ретрае ключ один на весь retry-chain | 1% | FI | -| 9.3 | Отсутствие ключа = отсутствие ретрая на non-idempotent статусах | 1% | FI | +| 9.1 | Public write methods accept `idempotency_key` | 1% | AC, CB (reference — Stripe) | +| 9.2 | The same key is used throughout the entire retry chain | 1% | FI | +| 9.3 | Absence of key = no retry on non-idempotent statuses | 1% | FI | -**Процедура.** Сценарий `[transport_error, 200]` для POST: с ключом — два запроса с одинаковым header, без ключа — одна попытка и исключение. +**Procedure.** Scenario `[transport_error, 200]` for POST: with key — two requests with the same header; without key — one attempt and exception. --- -### 17. Критерий 10. Пагинация и работа с коллекциями — 4% +### 17. Criterion 10. Pagination and Collection Handling — 4% -**Цель.** Большие коллекции не требуют ручной склейки страниц. +**Goal.** Large collections don't require manual page stitching. -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 10.1 | Lazy-контейнер возвращается по умолчанию | 1% | AC, FI | -| 10.2 | Итерация первых N элементов → `ceil(N/page)` запросов | 1% | FI, AM | -| 10.3 | Явная полная материализация без повторных запросов | 1% | FI | -| 10.4 | Пустая коллекция не делает лишних запросов; ошибка страницы N пробрасывается при чтении N | 1% | FI | +| 10.1 | Lazy container returned by default | 1% | AC, FI | +| 10.2 | Iterating first N elements → `ceil(N/page)` requests | 1% | FI, AM | +| 10.3 | Explicit full materialization without repeated requests | 1% | FI | +| 10.4 | Empty collection makes no extra requests; page N error propagates when reading N | 1% | FI | -**Процедура.** Mock-сервер с N страницами: замер количества HTTP-обращений при срезе `[:k]`, при полной материализации, при пустом ответе, при ошибке на странице 3. +**Procedure.** Mock server with N pages: count HTTP calls for a slice `[:k]`, for full materialization, for empty response, for error on page 3. --- -### 18. Критерий 11. Per-operation overrides — 3% +### 18. Criterion 11. Per-operation Overrides — 3% -**Цель.** Гибкость на уровне отдельного вызова без мутации клиента. +**Goal.** Flexibility at the individual call level without mutating the client. -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 11.1 | `timeout` можно переопределить на уровне метода | 1% | AC, FI | -| 11.2 | Retry-policy можно отключить/усилить для одного вызова | 1% | AC, FI | -| 11.3 | Override не мутирует клиент и не течёт между вызовами | 1% | FI | +| 11.1 | `timeout` can be overridden at the method level | 1% | AC, FI | +| 11.2 | Retry policy can be disabled/strengthened for a single call | 1% | AC, FI | +| 11.3 | Override does not mutate the client and does not leak between calls | 1% | FI | -**Процедура.** Передать `timeout=0.001` одному вызову, проверить, что соседний вызов с дефолтным таймаутом не пострадал. +**Procedure.** Pass `timeout=0.001` to one call, verify that a neighboring call with the default timeout is not affected. --- -### 19. Критерий 12. Async/sync parity — 3% +### 19. Criterion 12. Async/Sync Parity — 3% -Применимо только если SDK объявляет async-поверхность; иначе критерий отключается и вес перераспределяется пропорционально. +Applicable only if the SDK declares an async surface; otherwise the criterion is disabled and the weight is redistributed proportionally. -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 12.1 | Async и sync живут в разных namespace'ах | 1% | AC, CB | -| 12.2 | Сигнатуры идентичны (отличается только `async`/`await`) | 1% | AC, AM | -| 12.3 | Feature parity в рамках одного релиза | 1% | DA | +| 12.1 | Async and sync live in separate namespaces | 1% | AC, CB | +| 12.2 | Signatures are identical (differ only by `async`/`await`) | 1% | AC, AM | +| 12.3 | Feature parity within a single release | 1% | DA | --- -### 20. Критерий 13. Конфигурация и fail-fast — 4% +### 20. Criterion 13. Configuration and Fail-fast — 4% -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 13.1 | Конфигурация из environment, явного объекта и прямых аргументов — все три пути работают | 1% | AC, TT | -| 13.2 | Priority resolution (`env > .env > defaults`) детерминирована и документирована | 1% | FI, DA | -| 13.3 | Отсутствие обязательных полей ловится до первого HTTP | 1% | FI | -| 13.4 | Универсальные имена env (`TOKEN`, `SECRET`) не являются официальными алиасами | 1% | DA | +| 13.1 | Configuration from environment, explicit object, and direct arguments — all three paths work | 1% | AC, TT | +| 13.2 | Priority resolution (`env > .env > defaults`) is deterministic and documented | 1% | FI, DA | +| 13.3 | Missing required fields are caught before the first HTTP call | 1% | FI | +| 13.4 | Generic env names (`TOKEN`, `SECRET`) are not official aliases | 1% | DA | --- -### 21. Критерий 14. Покрытие контракта API — 4% +### 21. Criterion 14. API Contract Coverage — 4% -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 14.1 | Все операции upstream-спеки имеют публичный метод | 2% | AM, DA | -| 14.2 | Имена полей, nullability, enum'ы совпадают со спекой | 1% | DA | -| 14.3 | Устаревшие операции явно помечены deprecation | 1% | AC, DA | +| 14.1 | All operations in the upstream spec have a public method | 2% | AM, DA | +| 14.2 | Field names, nullability, and enums match the spec | 1% | DA | +| 14.3 | Deprecated operations are explicitly marked with deprecation | 1% | AC, DA | -**Процедура.** Из OpenAPI/Swagger извлечь список `operationId`; сопоставить с публичными методами через таблицу соответствия; на выборке 20 моделей сверить поля со схемой. +**Procedure.** Extract the list of `operationId` from OpenAPI/Swagger; map to public methods via a correspondence table; cross-check fields against schema on a sample of 20 models. --- -### 22. Критерий 15. Документация (Diátaxis) — 7% +### 22. Criterion 15. Documentation (Diátaxis) — 7% -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 15.1 | Tutorial (step-by-step от нуля до первого успеха) присутствует | 1.5% | DA, TT | Прогон персоны P1 по tutorial | -| 15.2 | How-to guides на типовые задачи | 1.5% | DA | Инвентаризация готовых рецептов | -| 15.3 | Reference покрывает публичный API | 1.5% | AM | `interrogate` — покрытие docstring'ами | -| 15.4 | Explanations на ключевые концепты (транспорт, retry, пагинация) | 1% | DA | -| 15.5 | CHANGELOG ведётся, связан с релизами | 0.5% | DA | -| 15.6 | Примеры в документации исполняемы | 1% | TT | Copy-paste всех snippet'ов в venv | +| 15.1 | Tutorial (step-by-step from zero to first success) is present | 1.5% | DA, TT | Run P1 persona through the tutorial | +| 15.2 | How-to guides for typical tasks | 1.5% | DA | Inventory of ready-made recipes | +| 15.3 | Reference covers the public API | 1.5% | AM | `interrogate` — docstring coverage | +| 15.4 | Explanations for key concepts (transport, retry, pagination) | 1% | DA | +| 15.5 | CHANGELOG is maintained, linked to releases | 0.5% | DA | +| 15.6 | Documentation examples are executable | 1% | TT | Copy-paste all snippets into a venv | -**Процедура.** Построить матрицу Diátaxis (4 столбца, по строкам — разделы документации). Каждый раздел попадает ровно в одну ячейку. Пустые столбцы = провал. +**Procedure.** Build a Diátaxis matrix (4 columns, documentation sections as rows). Each section belongs to exactly one cell. Empty columns = failure. --- -### 23. Критерий 16. Тестируемость пользовательским кодом — 5% +### 23. Criterion 16. Testability by User Code — 5% -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 16.1 | Публичный fake/mock-transport доступен из документированного namespace'а | 2% | AC, DA | -| 16.2 | Документирован контракт мока: скрипт ответов, инспекция вызовов, инъекция ошибок | 1% | DA | -| 16.3 | Публичные модели стабильно сериализуются через `json.dumps` без кастомных энкодеров | 1% | AM | -| 16.4 | Context-manager закрывает ресурсы, обращение после закрытия даёт понятную ошибку | 1% | FI | +| 16.1 | Public fake/mock transport is available from a documented namespace | 2% | AC, DA | +| 16.2 | Mock contract is documented: response script, call inspection, error injection | 1% | DA | +| 16.3 | Public models serialize stably via `json.dumps` without custom encoders | 1% | AM | +| 16.4 | Context manager closes resources; calling after close gives a clear error | 1% | FI | -**Процедура.** Написать «каноничный потребительский тест»: создать клиент с публичным моком, прогнать 3 сценария, сериализовать результаты в JSON, закрыть клиент, попытаться вызвать метод — зафиксировать поведение. +**Procedure.** Write a "canonical consumer test": create a client with the public mock, run 3 scenarios, serialize results to JSON, close the client, attempt to call a method — record behavior. --- -### 24. Критерий 17. Observability и логирование — 3% +### 24. Criterion 17. Observability and Logging — 3% -| # | Подкритерий | Вес | Методы | Инструменты | +| # | Sub-criterion | Weight | Methods | Tools | |---|---|---|---|---| -| 17.1 | SDK использует стандартный `logging` под именованным logger'ом | 1% | AC, LA | -| 17.2 | Структурированные поля: operation, endpoint, status, attempt, latency | 1% | LA | -| 17.3 | Каждый сетевой вызов оставляет хотя бы одну запись на `DEBUG` | 1% | LA | +| 17.1 | SDK uses standard `logging` under a named logger | 1% | AC, LA | +| 17.2 | Structured fields: operation, endpoint, status, attempt, latency | 1% | LA | +| 17.3 | Every network call leaves at least one record at `DEBUG` | 1% | LA | -**Процедура.** Включить `logging` на `DEBUG`, прогнать 5 типовых сценариев, выгрузить записи в JSON-lines, проверить наличие полей и отсутствие секретов. +**Procedure.** Enable `logging` at `DEBUG`, run 5 typical scenarios, export records as JSON lines, verify field presence and absence of secrets. --- -### 25. Критерий 18. Совместимость и deprecation policy — 5% +### 25. Criterion 18. Compatibility and Deprecation Policy — 5% -| # | Подкритерий | Вес | Методы | +| # | Sub-criterion | Weight | Methods | |---|---|---|---| -| 18.1 | Semver соблюдается (breaking → major, additive → minor, fix → patch) | 1% | DA | -| 18.2 | Deprecation-период явный, не менее двух минорных релизов | 1% | DA | -| 18.3 | Устаревшие символы выдают `DeprecationWarning` с ссылкой на замену | 1% | AC, FI | -| 18.4 | CHANGELOG содержит секции `Added`/`Changed`/`Deprecated`/`Removed`/`Fixed` | 1% | DA | -| 18.5 | Публичные переименования всегда проходят через alias с warning'ом | 1% | AC, DA | +| 18.1 | Semver is followed (breaking → major, additive → minor, fix → patch) | 1% | DA | +| 18.2 | Deprecation period is explicit, at least two minor releases | 1% | DA | +| 18.3 | Deprecated symbols emit `DeprecationWarning` with a link to the replacement | 1% | AC, FI | +| 18.4 | CHANGELOG contains sections `Added`/`Changed`/`Deprecated`/`Removed`/`Fixed` | 1% | DA | +| 18.5 | Public renames always go through an alias with a warning | 1% | AC, DA | -**Процедура.** Взять последние 3–5 релизов; для каждого — сопоставить характер изменений с версионированием. Импортировать устаревшие символы, проверить `DeprecationWarning`. +**Procedure.** Take the last 3–5 releases; for each — match the nature of changes against the versioning. Import deprecated symbols, verify `DeprecationWarning`. --- -## 26. Процесс оценки +## 26. Evaluation Process -Рекомендуемая длительность полного цикла — 1–2 дня на одного оценщика; 3–4 дня с участием пользователей. +Recommended duration for a full cycle — 1–2 days per evaluator; 3–4 days with user participation. -### 26.1 Подготовка (30 мин) +### 26.1 Preparation (30 min) -- Чистый venv, фиксация SHA/версии SDK. -- Установка инструментов из §5. -- Создание рабочей папки `evaluation//` для артефактов. +- Clean venv, fix SDK SHA/version. +- Install tools from §5. +- Create working folder `evaluation//` for artifacts. -### 26.2 Автоматический слой (1–2 часа) +### 26.2 Automated Layer (1–2 hours) -Последовательно прогнать и сохранить вывод: +Run sequentially and save output: - `ruff check`, `ruff format --check` -- `mypy --strict` или `pyright --strict` +- `mypy --strict` or `pyright --strict` - `bandit -r` - `vulture` - `interrogate` -- собственный скрипт сравнения upstream-спеки с публичным API +- custom script comparing upstream spec with public API -Заполнить все подкритерии с методом `SA`/`AM`. +Fill all sub-criteria with `SA`/`AM` method. -### 26.3 Документ-аудит (1–2 часа) +### 26.3 Document Audit (1–2 hours) -- Построить матрицу Diátaxis. -- Сверить список env-переменных в документации с фактическим поведением. -- Пройти CHANGELOG за последние релизы. -- Проверить исполняемость всех snippet'ов из документации. +- Build the Diátaxis matrix. +- Cross-check the list of env variables in documentation against actual behavior. +- Review CHANGELOG for recent releases. +- Verify executability of all documentation snippets. -### 26.4 Fault Injection (2–3 часа) +### 26.4 Fault Injection (2–3 hours) -- Поднять `pytest-httpserver` или `respx`. -- Прогнать матрицы: `(статус × сценарий) → ожидаемое поведение`. -- Зафиксировать результаты в таблицу. +- Spin up `pytest-httpserver` or `respx`. +- Run matrices: `(status × scenario) → expected behavior`. +- Record results in a table. -### 26.5 Персоны-сессии (2×30 мин, опционально 3×30) +### 26.5 Persona Sessions (2×30 min, optionally 3×30) -- P1 получает канонический онбординг-таск. -- P2 получает задачу «напиши тест поверх SDK». -- P3 (при наличии — опционально по diary study) фиксирует инциденты обновления версий. -- Запись экрана, протокол think-aloud. +- P1 receives the canonical onboarding task. +- P2 receives the task "write a test on top of the SDK". +- P3 (if available — optional via diary study) records version upgrade incidents. +- Screen recording, think-aloud protocol. -### 26.6 Эвристический проход (1 час) +### 26.6 Heuristic Pass (1 hour) -По 10 эвристикам DX (§5.5). Каждая эвристика даёт короткий observation + ссылку на затронутые подкритерии. +Using 10 DX heuristics (§5.5). Each heuristic produces a short observation + reference to affected sub-criteria. -### 26.7 Comparative benchmark (1 час) +### 26.7 Comparative Benchmark (1 hour) -Выбрать 2–3 эталонных SDK из §5.6. На тех же сценариях зафиксировать расхождения. +Select 2–3 reference SDKs from §5.6. Record divergences on the same scenarios. -### 26.8 Свод и отчёт (1–2 часа) +### 26.8 Synthesis and Report (1–2 hours) -- Заполнить итоговую таблицу `criterion × subcriterion × grade × evidence`. -- Вычислить `Score`. -- Вычислить `Pain` для каждого подкритерия с `grade < 0.75`. -- Отсортировать backlog. -- Оформить отчёт по шаблону §27. +- Fill the summary table `criterion × subcriterion × grade × evidence`. +- Calculate `Score`. +- Calculate `Pain` for each sub-criterion with `grade < 0.75`. +- Sort backlog. +- Format report following the template in §27. --- -## 27. Шаблон отчёта +## 27. Report Template -Фиксированная структура, чтобы результаты сравнимы между релизами. +Fixed structure to make results comparable across releases. -### 27.1 Резюме +### 27.1 Summary -- **Score**: `XX%` (категория: эталонный / рабочий / долг / рефакторинг). -- **Дата оценки**, **версия SDK**, **имя оценщика**. -- Топ-3 выводов в 1–2 предложениях. +- **Score**: `XX%` (category: reference / working / debt / refactoring). +- **Evaluation date**, **SDK version**, **evaluator name**. +- Top 3 findings in 1–2 sentences. -### 27.2 Сводная таблица критериев +### 27.2 Criteria Summary Table -Колонки: `#`, критерий, вес, средневзвешенный балл, вклад в `Score`, тренд (`↑/↓/→` относительно предыдущей оценки). +Columns: `#`, criterion, weight, weighted-average score, contribution to `Score`, trend (`↑/↓/→` relative to previous evaluation). -### 27.3 Развёрнутые наблюдения по критериям +### 27.3 Detailed Observations by Criterion -Для каждого критерия: +For each criterion: -- Баллы подкритериев с кратким обоснованием. -- Доказательство (`evidence`) — ссылка на лог, скриншот, цитата сообщения, запись сессии. **Без указания конкретных имён файлов из исходников** — описывается поведение, не локация. -- Что даст +25% на следующую оценку. +- Sub-criterion scores with brief justification. +- Evidence — link to log, screenshot, quoted message, session recording. **Without specifying concrete filenames from source code** — behavior is described, not location. +- What will earn +25% in the next evaluation. -### 27.4 Топ-10 по индексу боли +### 27.4 Top-10 by Pain Index -Таблица: `#`, наблюдение, подкритерий, `Pain`, оценка трудоёмкости исправления, рекомендация. +Table: `#`, observation, sub-criterion, `Pain`, estimated fix effort, recommendation. -### 27.5 Приложения +### 27.5 Appendices -- Сырые выводы инструментов (`ruff.txt`, `mypy.txt`, `bandit.json`). -- FI-матрица с запрошенными/полученными реакциями. -- Транскрипты персон-сессий (де-идентифицированные). -- DevSUS-анкеты. +- Raw tool output (`ruff.txt`, `mypy.txt`, `bandit.json`). +- FI matrix with requested/received reactions. +- Persona session transcripts (de-identified). +- DevSUS questionnaires. --- -## 28. Валидация методики +## 28. Methodology Validation -Методика считается пригодной, если: +The methodology is considered valid if: -1. **Воспроизводимость.** Два независимых оценщика получают `Score` в пределах ±5% на одной ревизии. -2. **Стабильность Pain-ranking.** Топ-10 подкритериев по `Pain` совпадает между сессиями ≥80%. -3. **Автоматизация.** Каждый подкритерий имеет хотя бы один объективный источник данных (SA/FI/DA/AM/CB). Чисто субъективных подкритериев нет. -4. **Предсказательность.** Выполнение всех рекомендаций из backlog даёт прирост `Score` не меньше суммы весов соответствующих подкритериев. -5. **Переносимость.** Методика применима к любому Python-SDK без изменений, кроме отключения критерия 12 (async/sync parity), если SDK синхронный. +1. **Reproducibility.** Two independent evaluators produce `Score` within ±5% on the same revision. +2. **Pain-ranking stability.** The top-10 sub-criteria by `Pain` agree between sessions ≥80%. +3. **Automation.** Each sub-criterion has at least one objective data source (SA/FI/DA/AM/CB). No purely subjective sub-criteria. +4. **Predictability.** Implementing all backlog recommendations produces a `Score` increase no less than the sum of weights of the corresponding sub-criteria. +5. **Portability.** The methodology applies to any Python SDK without changes, except for disabling criterion 12 (async/sync parity) for synchronous SDKs. --- -## 29. Регламент применения +## 29. Application Schedule -- **Ежеквартально** для стабильных SDK. -- **Перед каждым major-релизом** как гейт выпуска. -- **После любой архитектурной перестройки** публичного API. -- **После появления negative feedback** от пользователей — целевая оценка критериев, на которые указал feedback. +- **Quarterly** for stable SDKs. +- **Before every major release** as a release gate. +- **After any architectural rework** of the public API. +- **After negative user feedback appears** — targeted evaluation of the criteria indicated by the feedback. -Результаты — `Score`, дельта, Pain-топ — публикуются в release notes как часть описания качества релиза. +Results — `Score`, delta, Pain-top — are published in release notes as part of the release quality description. From 279689afa620436b4949e3a41d458cbe1d876de3 Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Fri, 24 Apr 2026 00:52:29 +0300 Subject: [PATCH 4/9] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 1 + avito/client.py | 15 ++++++++ avito/testing/fake_transport.py | 34 ++++++++++++++++- docs/site/explanations/architecture.md | 20 ++++++++-- docs/site/explanations/index.md | 30 +++++++-------- docs/site/how-to/auth-and-config.md | 49 +++++++++++++++++++++++-- docs/site/how-to/index.md | 36 ++++++++---------- docs/site/tutorials/first-promotion.md | 40 ++++++++++++++++---- tests/contracts/test_testing_api.py | 24 ++++++++++++ tests/docs/conftest.py | 36 ++++++++++++++++++ tests/docs/test_docs_harness_surface.py | 25 +++++++++++++ tests/docs/test_markdown_examples.py | 28 ++++++++++++++ tests/docs/test_no_placeholders.py | 20 ++++++++++ 13 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 tests/contracts/test_testing_api.py create mode 100644 tests/docs/conftest.py create mode 100644 tests/docs/test_docs_harness_surface.py create mode 100644 tests/docs/test_markdown_examples.py create mode 100644 tests/docs/test_no_placeholders.py diff --git a/Makefile b/Makefile index 9b95ea2..7c40fe2 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,7 @@ docs-serve: docs-strict: poetry run mkdocs build --strict poetry run python scripts/check_readme_domain_coverage.py + poetry run pytest tests/docs/ docs-build: docs-strict diff --git a/avito/client.py b/avito/client.py index 9f4a6e6..e21b151 100644 --- a/avito/client.py +++ b/avito/client.py @@ -74,6 +74,21 @@ def from_env(cls, *, env_file: str | Path | None = ".env") -> AvitoClient: return cls(AvitoSettings.from_env(env_file=env_file)) + @classmethod + def _from_transport( + cls, + settings: AvitoSettings, + *, + transport: Transport, + auth_provider: AuthProvider, + ) -> AvitoClient: + client = cls.__new__(cls) + client._closed = False + client.settings = settings + client.auth_provider = auth_provider + client.transport = transport + return client + def auth(self) -> AuthProvider: """Возвращает объект аутентификации и token-flow операций.""" diff --git a/avito/testing/fake_transport.py b/avito/testing/fake_transport.py index b2e84ff..2d6cd20 100644 --- a/avito/testing/fake_transport.py +++ b/avito/testing/fake_transport.py @@ -8,7 +8,8 @@ import httpx -from avito.auth import AuthSettings +from avito.auth import AuthProvider, AuthSettings +from avito.client import AvitoClient from avito.config import AvitoSettings from avito.core import Transport from avito.core.retries import RetryPolicy @@ -91,6 +92,37 @@ def build( sleep=lambda _: None, ) + def as_client( + self, + *, + user_id: int | None = None, + retry_policy: RetryPolicy | None = None, + ) -> AvitoClient: + """Создает публичный `AvitoClient` поверх fake transport без реального HTTP.""" + + auth_settings = AuthSettings(client_id="fake-client-id", client_secret="fake-client-secret") + settings = AvitoSettings( + base_url=self.base_url, + user_id=user_id, + auth=auth_settings, + retry_policy=retry_policy or RetryPolicy(), + timeouts=ApiTimeouts(), + ) + auth_provider = AuthProvider(auth_settings) + transport = Transport( + settings, + auth_provider=None, + client=httpx.Client( + transport=httpx.MockTransport(self._handle), base_url=self.base_url + ), + sleep=lambda _: None, + ) + return AvitoClient._from_transport( + settings, + transport=transport, + auth_provider=auth_provider, + ) + def count(self, *, method: str | None = None, path: str | None = None) -> int: return len( [ diff --git a/docs/site/explanations/architecture.md b/docs/site/explanations/architecture.md index 278a47b..659418a 100644 --- a/docs/site/explanations/architecture.md +++ b/docs/site/explanations/architecture.md @@ -1,9 +1,6 @@ # Архитектура SDK -!!! note "Раздел в разработке" - Полное объяснение архитектуры будет добавлено в PR 3. - - Сейчас страница содержит минимальную Mermaid-диаграмму, чтобы строгая сборка сайта проверяла поддержку диаграмм в MkDocs Material. +SDK построен вокруг одного публичного фасада `AvitoClient`. Он создаёт доменные объекты, а доменные объекты делегируют HTTP-операции section client-ам. Transport отвечает за `httpx`, retry, token injection и маппинг ошибок. Mappers преобразуют JSON в публичные dataclass-модели. ```mermaid flowchart LR @@ -16,3 +13,18 @@ flowchart LR section --> mapper[Mapper] mapper --> model[SDK model] ``` + +Такое разделение удерживает публичный API простым: пользовательский код работает с доменными объектами и typed-моделями, но не управляет заголовками, refresh token-flow, retry-циклами или JSON-маппингом вручную. + +## Границы слоёв + +| Слой | Ответственность | +|---|---| +| `AvitoClient` | Единая точка входа, context manager, фабрики доменных объектов | +| Domain object | Публичные методы конкретного сценария, например `account().get_self()` | +| Section client | HTTP path, method, payload и выбор mapper-а для одного API-раздела | +| `Transport` | `httpx.Client`, retry, timeouts, auth header, error mapping | +| `AuthProvider` | Получение, кэширование и инвалидирование токенов | +| Mapper | Преобразование JSON-ответа в публичную SDK-модель | + +Публичные методы не возвращают raw `dict` и не принимают transport-layer request DTO. Если операция требует сложный payload, доменный метод раскрывает понятные keyword-only параметры или публичную модель, закреплённую в reference. diff --git a/docs/site/explanations/index.md b/docs/site/explanations/index.md index bcef10f..3073ac7 100644 --- a/docs/site/explanations/index.md +++ b/docs/site/explanations/index.md @@ -1,19 +1,15 @@ # Explanations -!!! note "Раздел в разработке" - Концептуальные статьи будут добавлены в PR 3. Они объясняют архитектурные решения и инварианты SDK. - - **Запланированные статьи:** - - | Статья | Описание | - |---|---| - | Архитектура SDK | Слои, их границы, почему они именно такие | - | Transport | Как устроен httpx-слой, retry, error mapping | - | Retry и backoff | Sequence-диаграмма поведения при 5xx/429 | - | Семантика пагинации | Flow подгрузки страниц, гарантии lazy loading | - | Контракт dry_run | Ветвление после `build_payload()`, что не вызывается | - | Модель ошибок | Почему 401 и 403 — siblings, не parent/child | - | Обратная совместимость | SemVer, политика deprecation, `DeprecationWarning` | - | Только синхронный API | Почему нет async, где будет `avito.aio` | - - Пока изучите [README](https://github.com/p141592/avito_python_api#readme) для общего обзора. +Explanations описывают причины архитектурных решений SDK: границы слоёв, retry-поведение, модель ошибок, безопасность диагностики и правила обратной совместимости. + +| Статья | Что объясняет | +|---|---| +| [Архитектура SDK](architecture.md) | Как `AvitoClient`, домены, section clients, transport, auth и mappers разделяют ответственность | +| Transport и retry | Почему retry живёт в transport-слое и как учитываются 429/5xx | +| Модель ошибок | Как HTTP-коды превращаются в typed exceptions | +| Семантика пагинации | Почему `PaginatedList` ленивый и когда загружаются страницы | +| Dry-run и идемпотентность | Как write-операции проверяются без сетевого вызова | +| Security и redaction | Какие секреты SDK не раскрывает в диагностике и ошибках | +| Совместимость | Как deprecation отражается в runtime, reference и changelog | + +Для практических сценариев используйте [how-to рецепты](../how-to/index.md), а для полного API-контракта — [reference](../reference/index.md). diff --git a/docs/site/how-to/auth-and-config.md b/docs/site/how-to/auth-and-config.md index 239ccae..fd6dc50 100644 --- a/docs/site/how-to/auth-and-config.md +++ b/docs/site/how-to/auth-and-config.md @@ -1,8 +1,49 @@ # Авторизация и конфигурация -!!! note "Раздел в разработке" - Полный рецепт будет добавлен в PR 3 после включения проверки исполняемых примеров документации. +SDK поддерживает три публичных способа создать клиент. Для приложений и CLI обычно удобен `AvitoClient.from_env()`: секреты остаются вне кода, а конфигурация читается одинаково локально и в CI. - Страница будет покрывать `AvitoClient.from_env()`, явные `client_id` и `client_secret`, `AvitoSettings`, `AuthSettings`, приоритет переменных окружения и безопасную диагностику конфигурации. +```bash +export AVITO_CLIENT_ID="client-id" +export AVITO_CLIENT_SECRET="client-secret" +``` -Пока используйте [быстрый старт](../tutorials/getting-started.md). +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + profile = avito.account().get_self() + +print(profile.user_id) +``` + +Для коротких скриптов можно передать ключи явно: + +```python +from avito import AvitoClient + +with AvitoClient(client_id="client-id", client_secret="client-secret") as avito: + info = avito.debug_info() + +print(info.base_url) +``` + +Для полного контроля используйте `AvitoSettings` и `AuthSettings`. + +```python +from avito import AuthSettings, AvitoClient, AvitoSettings + +settings = AvitoSettings( + user_id=123, + auth=AuthSettings( + client_id="client-id", + client_secret="client-secret", + ), +) + +with AvitoClient(settings) as avito: + info = avito.debug_info() + +print(info.user_id) +``` + +Значения из process environment имеют приоритет над `.env`. Если обязательные ключи отсутствуют, SDK поднимает `ConfigurationError` при создании клиента, до первого HTTP-запроса. Безопасный снимок `debug_info()` не содержит `client_secret`, access token или заголовок `Authorization`. diff --git a/docs/site/how-to/index.md b/docs/site/how-to/index.md index 0a4817d..dc15a11 100644 --- a/docs/site/how-to/index.md +++ b/docs/site/how-to/index.md @@ -1,25 +1,19 @@ # How-to рецепты -!!! note "Раздел в разработке" - Рецепты будут добавлены в PR 3. Каждый рецепт решает одну конкретную задачу с минимальным кодом. +How-to раздел собирает рецепты для конкретных задач интеграции. Каждый рецепт показывает один рабочий сценарий, минимальный набор настроек и ссылки на reference-страницы с полным контрактом. - **Запланированные рецепты:** +| Рецепт | Задача | +|---|---| +| [Авторизация и конфигурация](auth-and-config.md) | Создать клиент через env, явные ключи или `AvitoSettings` | +| Загрузка изображений в чат | Отправить multipart-upload через `chat_media()` | +| Продвижение с dry-run | Проверить payload write-операции без сетевого вызова | +| Пагинация | Читать `PaginatedList` частями и материализовать весь список | +| Этикетки заказов | Создать задачу генерации и скачать PDF | +| Отклики на вакансии | Получить список откликов с фильтрами | +| Отчёт Автотеки | Пройти цепочку VIN → preview → report | +| Бронирование недвижимости | Обновить блокировки дат и цены | +| CPA и CallTracking | Найти звонки и скачать запись | +| Рейтинги и тарифы | Получить отзывы, ответы и тарифную информацию | +| Тестирование с FakeTransport | Проверить consumer-код без реального HTTP | - | Рецепт | Описание | - |---|---| - | Авторизация и конфигурация | Все способы создания клиента, env-переменные, `AvitoSettings` | - | Загрузка изображений в чат | Multipart-upload через `chat_media()` | - | Продвижение с dry_run | Сухой прогон перед реальной операцией | - | Пагинация | Ленивая загрузка, slicing, `materialize()` | - | Этикетки заказов | Создание задачи, polling, скачивание PDF | - | Отклики на вакансии | Список откликов с фильтрацией | - | Отчёт Автотеки | VIN → preview → full report | - | Бронирование недвижимости | Блокировка дат, обновление цен | - | CPA и CallTracking | Фильтрация звонков, скачивание записей | - | Рейтинги и тарифы | Отзывы, тарифные пакеты | - | Per-operation таймауты | Переопределение таймаутов на уровне запроса | - | Идемпотентность | Idempotency-key в write-операциях | - | Тестирование с FakeTransport | Изоляция тестов без HTTP | - | Отладка и логирование | `debug_info()`, диагностика конфигурации | - - Пока изучите [Быстрый старт](../tutorials/getting-started.md) и [Reference](../reference/index.md). +Для первого запуска начните с [быстрого старта](../tutorials/getting-started.md). Для поиска метода по операции API используйте [карту операций](../reference/operations.md). diff --git a/docs/site/tutorials/first-promotion.md b/docs/site/tutorials/first-promotion.md index e72106f..84bdcaf 100644 --- a/docs/site/tutorials/first-promotion.md +++ b/docs/site/tutorials/first-promotion.md @@ -1,13 +1,37 @@ # Первое продвижение -!!! note "Раздел в разработке" - Этот раздел будет добавлен в PR 3. Он покроет создание кампании автостратегии с использованием сухого прогона (`dry_run=True`) перед реальным запуском. +Этот tutorial показывает безопасный порядок запуска write-сценария: сначала собрать и проверить параметры, затем выполнить реальный запрос. Для продвижения это особенно важно, потому что ошибка в датах, бюджете или списке объявлений может затронуть деньги. - **Что будет здесь:** +## Подготовка - - Создание бюджетной кампании через `autostrategy_campaign().create_budget(...)` - - Проверка параметров без сетевого запроса: `dry_run=True` - - Запуск реальной кампании: `dry_run=False` - - Чтение `PromotionActionResult` и интерпретация статусов +Проверьте, что клиент создаётся из окружения и аккаунт доступен: - Пока используйте [Reference: AutostrategyCampaign](../reference/index.md) для изучения API. +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + profile = avito.account().get_self() + +print(profile.user_id) +``` + +## Сухой прогон + +Write-операции, которые принимают `dry_run`, строят тот же payload, что и обычный вызов, но не отправляют запрос в transport. Это позволяет проверить consumer-код, сериализацию параметров и обработку результата до реального запуска. + +```text +with AvitoClient.from_env() as avito: + preview = avito.autostrategy_campaign().create_budget( + campaign_type="AS", + start_time="2026-05-01T00:00:00Z", + finish_time="2026-05-07T00:00:00Z", + items=[1001, 1002], + dry_run=True, + ) +``` + +После проверки замените `dry_run=True` на `dry_run=False` или опустите параметр. Для повторяемых write-вызовов используйте `idempotency_key`, если метод его поддерживает. + +## Следующий шаг + +Полный список методов продвижения смотрите в [карте операций](../reference/operations.md) и reference-странице домена `promotion`. diff --git a/tests/contracts/test_testing_api.py b/tests/contracts/test_testing_api.py new file mode 100644 index 0000000..c8c9e87 --- /dev/null +++ b/tests/contracts/test_testing_api.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from avito import AvitoClient +from avito.testing import FakeTransport + + +def test_fake_transport_builds_public_client_without_real_http() -> None: + fake = FakeTransport() + fake.add_json( + "GET", + "/core/v1/accounts/self", + {"id": 7, "name": "Иван", "email": "user@example.com", "phone": "+7999"}, + ) + + with fake.as_client(user_id=7) as avito: + profile = avito.account().get_self() + info = avito.debug_info() + + assert isinstance(avito, AvitoClient) + assert profile.user_id == 7 + assert profile.name == "Иван" + assert info.user_id == 7 + assert info.requires_auth is False + assert fake.count(method="GET", path="/core/v1/accounts/self") == 1 diff --git a/tests/docs/conftest.py b/tests/docs/conftest.py new file mode 100644 index 0000000..ce11a68 --- /dev/null +++ b/tests/docs/conftest.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from collections.abc import Iterator + +import pytest + +from avito import AvitoClient +from avito.testing import FakeTransport + + +def build_docs_client() -> AvitoClient: + fake = FakeTransport() + fake.add_json( + "GET", + "/core/v1/accounts/self", + {"id": 7, "name": "Иван", "email": "user@example.com", "phone": "+7999"}, + ) + return fake.as_client(user_id=7) + + +@pytest.fixture(autouse=True) +def docs_client_from_env(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: + monkeypatch.setenv("AVITO_CLIENT_ID", "docs-client-id") + monkeypatch.setenv("AVITO_CLIENT_SECRET", "docs-client-secret") + + def from_env( + cls: type[AvitoClient], + *, + env_file: str | None = ".env", + ) -> AvitoClient: + _ = cls + _ = env_file + return build_docs_client() + + monkeypatch.setattr(AvitoClient, "from_env", classmethod(from_env)) + yield diff --git a/tests/docs/test_docs_harness_surface.py b/tests/docs/test_docs_harness_surface.py new file mode 100644 index 0000000..440ecb1 --- /dev/null +++ b/tests/docs/test_docs_harness_surface.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import inspect + +from avito import AvitoClient +from tests.docs.conftest import build_docs_client + + +def signature_without_self(callable_object: object) -> inspect.Signature: + signature = inspect.signature(callable_object) + parameters = [ + parameter + for name, parameter in signature.parameters.items() + if name != "self" + ] + return signature.replace(parameters=parameters) + + +def test_docs_harness_uses_real_public_client_surface() -> None: + client = build_docs_client() + account = client.account() + + assert isinstance(client, AvitoClient) + assert signature_without_self(type(client).account) == inspect.signature(client.account) + assert signature_without_self(type(account).get_self) == inspect.signature(account.get_self) diff --git a/tests/docs/test_markdown_examples.py b/tests/docs/test_markdown_examples.py new file mode 100644 index 0000000..72ce1f9 --- /dev/null +++ b/tests/docs/test_markdown_examples.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import re +from pathlib import Path + +DOCS_ROOT = Path(__file__).resolve().parents[2] +EXECUTABLE_MARKDOWN = [ + *sorted((DOCS_ROOT / "docs/site/tutorials").glob("*.md")), + *sorted((DOCS_ROOT / "docs/site/how-to").glob("*.md")), +] +PYTHON_BLOCK = re.compile(r"```(?:python|pycon)\n(.*?)\n```", re.DOTALL) + + +def executable_blocks(path: Path) -> list[str]: + return [match.group(1) for match in PYTHON_BLOCK.finditer(path.read_text(encoding="utf-8"))] + + +def test_tutorial_and_howto_python_examples_execute_without_network() -> None: + namespace: dict[str, object] = {} + blocks = [(path, block) for path in EXECUTABLE_MARKDOWN for block in executable_blocks(path)] + + assert blocks, "В tutorials/how-to должен быть хотя бы один исполняемый Python-пример." + + for path, block in blocks: + try: + exec(compile(block, str(path), "exec"), namespace) + except Exception as exc: # noqa: BLE001 + raise AssertionError(f"Python-пример из {path} не выполнился.") from exc diff --git a/tests/docs/test_no_placeholders.py b/tests/docs/test_no_placeholders.py new file mode 100644 index 0000000..5949812 --- /dev/null +++ b/tests/docs/test_no_placeholders.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import re +from pathlib import Path + +DOCS_ROOT = Path(__file__).resolve().parents[2] +PLACEHOLDER_PATTERN = re.compile( + r"Раздел в разработке|placeholder|плейсхолдер|TODO|TBD|coming soon", + re.IGNORECASE, +) + + +def test_production_docs_do_not_contain_placeholders() -> None: + offenders: list[str] = [] + for path in sorted((DOCS_ROOT / "docs/site").rglob("*.md")): + text = path.read_text(encoding="utf-8") + if PLACEHOLDER_PATTERN.search(text): + offenders.append(str(path.relative_to(DOCS_ROOT))) + + assert offenders == [] From 4f3c467db94994a38b15e51216bc671852c38cf0 Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Fri, 24 Apr 2026 01:18:11 +0300 Subject: [PATCH 5/9] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- avito/testing/fake_transport.py | 4 +- docs/site/how-to/.pages | 6 + docs/site/how-to/account-profile.md | 91 +++++ docs/site/how-to/ad-listing-and-stats.md | 107 ++++++ docs/site/how-to/chat-image-upload.md | 107 ++++++ docs/site/how-to/index.md | 10 +- docs/site/how-to/job-applications.md | 127 +++++++ docs/site/how-to/order-labels.md | 102 ++++++ docs/site/how-to/ratings-and-tariffs.md | 84 +++++ scripts/build_docs_quality_report.py | 26 +- tests/docs/conftest.py | 438 ++++++++++++++++++++++- 11 files changed, 1093 insertions(+), 9 deletions(-) create mode 100644 docs/site/how-to/account-profile.md create mode 100644 docs/site/how-to/ad-listing-and-stats.md create mode 100644 docs/site/how-to/chat-image-upload.md create mode 100644 docs/site/how-to/job-applications.md create mode 100644 docs/site/how-to/order-labels.md create mode 100644 docs/site/how-to/ratings-and-tariffs.md diff --git a/avito/testing/fake_transport.py b/avito/testing/fake_transport.py index 2d6cd20..2f8a80c 100644 --- a/avito/testing/fake_transport.py +++ b/avito/testing/fake_transport.py @@ -159,7 +159,9 @@ def _handle(self, request: httpx.Request) -> httpx.Response: if key not in self._routes: available = ", ".join(f"{method} {path}" for method, path in sorted(self._routes)) raise AssertionError( - f"Unexpected request {recorded.method} {recorded.path}. Known: {available}" + "Маршрут не прописан в FakeTransport: " + f"{recorded.method} {recorded.path}. " + f"Добавьте route_sequence или add_json для этого пути. Доступные: {available}" ) responders = self._routes[key] diff --git a/docs/site/how-to/.pages b/docs/site/how-to/.pages index e9b4ae6..8c118cc 100644 --- a/docs/site/how-to/.pages +++ b/docs/site/how-to/.pages @@ -1,3 +1,9 @@ nav: - index.md - auth-and-config.md + - account-profile.md + - ad-listing-and-stats.md + - chat-image-upload.md + - order-labels.md + - job-applications.md + - ratings-and-tariffs.md diff --git a/docs/site/how-to/account-profile.md b/docs/site/how-to/account-profile.md new file mode 100644 index 0000000..ac016c6 --- /dev/null +++ b/docs/site/how-to/account-profile.md @@ -0,0 +1,91 @@ +# Профиль, баланс и иерархия аккаунта + +Этот рецепт показывает рабочий минимум для раздела `accounts`: получить профиль авторизованного пользователя, проверить баланс, прочитать историю операций и посмотреть данные иерархии аккаунтов. + +## Профиль + +Используйте `account().get_self()`, когда нужно определить пользователя, от имени которого работает токен. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + profile = avito.account().get_self() + +print(profile.user_id) +print(profile.email) +``` + +## Баланс + +`get_balance()` требует `user_id`. Его можно передать в фабрику `account()` один раз или явно в сам метод. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + balance = avito.account(user_id=7).get_balance() + +print(balance.real) +print(balance.bonus) +print(balance.total) +``` + +## История операций + +История возвращается как `PaginatedList`: первая страница уже загружена, а остальные страницы читаются лениво при итерации или через `materialize()`. + +```python +from datetime import datetime, timezone + +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + history = avito.account(user_id=7).get_operations_history( + date_from=datetime(2026, 4, 1, tzinfo=timezone.utc), + limit=2, + ) + operations = history.materialize() + +print(operations[0].amount) +``` + +## Иерархия аккаунтов + +Для компаний и агентских кабинетов используйте отдельный доменный объект `account_hierarchy()`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + hierarchy = avito.account_hierarchy(user_id=7) + status = hierarchy.get_status() + employees = hierarchy.list_employees() + phones = hierarchy.list_company_phones() + +print(status.is_active) +print(employees.items[0].name) +print(phones.items[0].phone) +``` + +## Объявления сотрудника + +Операции иерархии принимают конкретные идентификаторы: `employee_id`, `item_ids`, `source_employee_id`. Для повторяемых write-вызовов передавайте `idempotency_key`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + hierarchy = avito.account_hierarchy(user_id=7) + result = hierarchy.link_items( + employee_id=10, + item_ids=[101], + idempotency_key="account-profile-example-1", + ) + items = hierarchy.list_items_by_employee(employee_id=10, limit=5) + +print(result.success) +print(items[0].title) +``` + +Полный контракт моделей смотрите в [reference по домену accounts](../reference/domains/accounts.md), а общие правила пагинации — в [reference по пагинации](../reference/pagination.md). diff --git a/docs/site/how-to/ad-listing-and-stats.md b/docs/site/how-to/ad-listing-and-stats.md new file mode 100644 index 0000000..2385675 --- /dev/null +++ b/docs/site/how-to/ad-listing-and-stats.md @@ -0,0 +1,107 @@ +# Объявления, статистика и продвижение + +Этот рецепт покрывает повседневный сценарий домена `ads`: найти объявления, открыть карточку, посмотреть статистику и подготовить безопасное действие продвижения. + +## Список объявлений + +`ad().list()` возвращает `PaginatedList[Listing]`. Первую страницу SDK загружает сразу, остальные страницы дочитываются при обращении по индексу, итерации или `materialize()`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + listings = avito.ad(user_id=7).list(status="active", limit=2) + items = listings.materialize() + +print(items[0].item_id) +print(items[0].title) +``` + +## Карточка объявления + +Для чтения одного объявления нужны оба идентификатора: `user_id` и `item_id`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + item = avito.ad(item_id=101, user_id=7).get() + +print(item.status) +print(item.price) +``` + +## Статистика объявлений + +`ad_stats()` группирует статистику, аналитику, звонки и расходы. Если `item_id` передан в фабрику, методы используют его по умолчанию. + +```python +from datetime import datetime, timezone + +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + stats = avito.ad_stats(item_id=101, user_id=7) + item_stats = stats.get_item_stats( + date_from=datetime(2026, 4, 1, tzinfo=timezone.utc), + date_to=datetime(2026, 4, 23, tzinfo=timezone.utc), + ) + calls = stats.get_calls_stats() + spendings = stats.get_account_spendings() + +print(item_stats.items[0].views) +print(calls.items[0].answered_calls) +print(spendings.total) +``` + +## Аналитика по списку объявлений + +Когда нужно сравнить несколько объявлений, передайте `item_ids` явно. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + analytics = avito.ad_stats(user_id=7).get_item_analytics( + item_ids=[101], + fields=["views", "contacts", "favorites"], + ) + +print(analytics.items[0].contacts) +``` + +## Цены продвижения и dry-run + +Перед включением услуг продвижения проверьте доступные цены. Для write-операций используйте `dry_run=True`, чтобы получить preview результата без вызова transport. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + promotion = avito.ad_promotion(item_id=101, user_id=7) + prices = promotion.get_vas_prices(item_ids=[101]) + preview = promotion.apply_vas(codes=["xl"], dry_run=True) + +print(prices.items[0].price) +print(preview.applied) +print(preview.request_payload) +``` + +## Обновление цены + +`update_price()` меняет цену конкретного объявления. Для повторяемых запусков передавайте `idempotency_key`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + result = avito.ad(item_id=101, user_id=7).update_price( + price=10900, + idempotency_key="ad-price-example-1", + ) + +print(result.status) +print(result.price) +``` + +Полный список методов домена смотрите в [reference по ads](../reference/domains/ads.md), а общие правила `PaginatedList` — в [reference по пагинации](../reference/pagination.md). diff --git a/docs/site/how-to/chat-image-upload.md b/docs/site/how-to/chat-image-upload.md new file mode 100644 index 0000000..6c6a2ae --- /dev/null +++ b/docs/site/how-to/chat-image-upload.md @@ -0,0 +1,107 @@ +# Чаты, сообщения и изображения + +Этот рецепт показывает базовый цикл работы с мессенджером: найти чат, прочитать сообщения, отправить текст, загрузить изображение и отправить его в чат. + +## Список чатов + +Для операций мессенджера почти всегда нужен `user_id`. Передайте его в фабрику доменного объекта. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + chats = avito.chat(user_id=7).list() + +print(chats.items[0].chat_id) +print(chats.items[0].last_message_text) +``` + +## Карточка и сообщения чата + +`chat()` отвечает за состояние чата, а `chat_message()` — за сообщения внутри него. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + chat = avito.chat("chat-1", user_id=7).get() + messages = avito.chat_message(chat_id="chat-1", user_id=7).list() + +print(chat.title) +print(messages.items[0].text) +``` + +## Отправка текста + +Для повторяемых write-вызовов передавайте `idempotency_key`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + sent = avito.chat_message(chat_id="chat-1", user_id=7).send_message( + message="Здравствуйте, объявление актуально.", + idempotency_key="chat-text-example-1", + ) + +print(sent.message_id) +print(sent.status) +``` + +## Загрузка изображения + +`upload_images()` принимает список `UploadImageFile`. В пользовательском коде `content` может быть bytes или file-like объектом. + +```python +from avito import AvitoClient +from avito.messenger import UploadImageFile + +image = UploadImageFile( + field_name="image", + filename="photo.jpg", + content=b"image-bytes", + content_type="image/jpeg", +) + +with AvitoClient.from_env() as avito: + uploaded = avito.chat_media(user_id=7).upload_images( + files=[image], + idempotency_key="chat-upload-example-1", + ) + +print(uploaded.items[0].image_id) +``` + +## Отправка изображения + +После загрузки используйте `image_id` в `send_image()`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + result = avito.chat_message(chat_id="chat-1", user_id=7).send_image( + image_id="img-1", + caption="Фото товара", + idempotency_key="chat-image-example-1", + ) + +print(result.message_id) +``` + +## Пометка чата прочитанным + +Состояние чата меняется через `chat().mark_read()`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + result = avito.chat("chat-1", user_id=7).mark_read( + idempotency_key="chat-read-example-1", + ) + +print(result.success) +``` + +Полный контракт моделей смотрите в [reference по messenger](../reference/domains/messenger.md). Для webhook и спецпредложений используйте отдельные доменные объекты `chat_webhook()` и `special_offer_campaign()`. diff --git a/docs/site/how-to/index.md b/docs/site/how-to/index.md index dc15a11..9ad66a6 100644 --- a/docs/site/how-to/index.md +++ b/docs/site/how-to/index.md @@ -5,15 +5,17 @@ How-to раздел собирает рецепты для конкретных | Рецепт | Задача | |---|---| | [Авторизация и конфигурация](auth-and-config.md) | Создать клиент через env, явные ключи или `AvitoSettings` | -| Загрузка изображений в чат | Отправить multipart-upload через `chat_media()` | +| [Профиль, баланс и иерархия аккаунта](account-profile.md) | Получить профиль, баланс, историю операций и данные сотрудников | +| [Объявления, статистика и продвижение](ad-listing-and-stats.md) | Найти объявления, открыть карточку, прочитать статистику и подготовить VAS | +| [Чаты, сообщения и изображения](chat-image-upload.md) | Отправить текст, загрузить изображение и отправить его в чат | | Продвижение с dry-run | Проверить payload write-операции без сетевого вызова | | Пагинация | Читать `PaginatedList` частями и материализовать весь список | -| Этикетки заказов | Создать задачу генерации и скачать PDF | -| Отклики на вакансии | Получить список откликов с фильтрами | +| [Заказы, этикетки и остатки](order-labels.md) | Создать задачу генерации, скачать PDF и обновить остатки | +| [Вакансии, отклики и резюме](job-applications.md) | Получить отклики, обработать их и настроить webhook | | Отчёт Автотеки | Пройти цепочку VIN → preview → report | | Бронирование недвижимости | Обновить блокировки дат и цены | | CPA и CallTracking | Найти звонки и скачать запись | -| Рейтинги и тарифы | Получить отзывы, ответы и тарифную информацию | +| [Рейтинги, отзывы и тарифы](ratings-and-tariffs.md) | Получить отзывы, ответить на отзыв и прочитать тариф | | Тестирование с FakeTransport | Проверить consumer-код без реального HTTP | Для первого запуска начните с [быстрого старта](../tutorials/getting-started.md). Для поиска метода по операции API используйте [карту операций](../reference/operations.md). diff --git a/docs/site/how-to/job-applications.md b/docs/site/how-to/job-applications.md new file mode 100644 index 0000000..152ac21 --- /dev/null +++ b/docs/site/how-to/job-applications.md @@ -0,0 +1,127 @@ +# Вакансии, отклики и резюме + +Этот рецепт показывает основной цикл раздела `jobs`: найти вакансии, получить отклики, изменить состояние отклика, посмотреть резюме и настроить webhook. + +## Вакансии + +`vacancy().list()` возвращает список вакансий, а `vacancy(vacancy_id).get()` открывает карточку конкретной вакансии. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + vacancies = avito.vacancy().list() + vacancy = avito.vacancy(101).get() + +print(vacancies.items[0].title) +print(vacancy.url) +``` + +## Идентификаторы откликов + +Для инкрементальной синхронизации используйте `ApplicationIdsQuery`: он возвращает id откликов, обновлённых после указанного момента. + +```python +from avito import AvitoClient +from avito.jobs import ApplicationIdsQuery + +query = ApplicationIdsQuery(updated_at_from="2026-04-23T00:00:00+03:00") + +with AvitoClient.from_env() as avito: + ids = avito.application().list(query=query) + +print(ids.items[0].id) +print(ids.cursor) +``` + +## Данные откликов + +Когда id уже известны, запросите подробные данные через тот же метод `list(ids=...)`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + applications = avito.application().list(ids=["app-1"]) + +print(applications.items[0].applicant_name) +print(applications.items[0].state) +``` + +## Обработка откликов + +Можно получить справочник состояний, отметить отклик просмотренным и выполнить действие над откликом. + +```python +from avito import AvitoClient +from avito.jobs import ApplicationViewedItem + +with AvitoClient.from_env() as avito: + states = avito.application().get_states() + viewed = avito.application().update( + applies=[ApplicationViewedItem(id="app-1", is_viewed=True)], + idempotency_key="job-viewed-example-1", + ) + invited = avito.application().apply( + ids=["app-1"], + action="invited", + idempotency_key="job-invite-example-1", + ) + +print(states.items[0].slug) +print(viewed.status) +print(invited.status) +``` + +## Резюме и контакты + +`resume().list()` ищет резюме, `resume(resume_id).get()` открывает карточку, а `get_contacts()` возвращает контактные данные. + +```python +from avito import AvitoClient +from avito.jobs import ResumeSearchQuery + +with AvitoClient.from_env() as avito: + resumes = avito.resume().list(query=ResumeSearchQuery(query="оператор")) + resume = avito.resume("res-1").get() + contacts = avito.resume("res-1").get_contacts() + +print(resumes.items[0].candidate_name) +print(resume.location) +print(contacts.phone) +``` + +## Webhook откликов + +Webhook позволяет получать события по откликам без polling. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + current = avito.job_webhook().get() + updated = avito.job_webhook().update( + url="https://example.com/job", + idempotency_key="job-webhook-example-1", + ) + +print(current.url) +print(updated.is_active) +``` + +## Словари вакансий + +Словари помогают выбирать значения для форм вакансий и фильтров. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + dictionaries = avito.job_dictionary().list() + values = avito.job_dictionary("profession").get() + +print(dictionaries.items[0].id) +print(values.items[0].name) +``` + +Полный контракт смотрите в [reference по jobs](../reference/domains/jobs.md). Для write-операций вакансий используйте `idempotency_key`, особенно при повторных запусках фоновых задач. diff --git a/docs/site/how-to/order-labels.md b/docs/site/how-to/order-labels.md new file mode 100644 index 0000000..312e000 --- /dev/null +++ b/docs/site/how-to/order-labels.md @@ -0,0 +1,102 @@ +# Заказы, этикетки и остатки + +Этот рецепт показывает основной рабочий цикл домена `orders`: прочитать заказы, выполнить действие по заказу, создать PDF-этикетку, проверить delivery task и обновить остатки. + +## Список заказов + +`order().list()` возвращает типизированный результат с краткой информацией по заказам. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + orders = avito.order().list() + +print(orders.items[0].order_id) +print(orders.items[0].buyer_name) +``` + +## Действия по заказу + +Write-операции принимают конкретный `order_id`. Для повторяемых действий передавайте `idempotency_key`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + confirmed = avito.order().apply( + order_id="ord-1", + transition="confirm", + idempotency_key="order-confirm-example-1", + ) + marked = avito.order().update_markings( + order_id="ord-1", + codes=["marking-code-1"], + idempotency_key="order-marking-example-1", + ) + +print(confirmed.status) +print(marked.success) +``` + +## Генерация этикетки + +Сначала создайте задачу генерации, затем скачайте PDF по `task_id`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + task = avito.order_label().create( + order_ids=["ord-1"], + idempotency_key="label-create-example-1", + ) + label = avito.order_label(task.task_id).download() + +print(label.filename) +print(label.binary.content_type) +``` + +## Доставка и delivery task + +Production delivery API возвращает идентификатор задачи или сущности. Статус задачи можно прочитать через `delivery_task()`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + announcement = avito.delivery_order().create_announcement( + order_id="ord-1", + idempotency_key="delivery-announcement-example-1", + ) + parcel = avito.delivery_order().create( + order_id="ord-1", + parcel_id="par-1", + idempotency_key="delivery-parcel-example-1", + ) + task = avito.delivery_task(announcement.task_id).get() + +print(parcel.parcel_id) +print(task.status) +``` + +## Остатки + +`stock()` читает и обновляет остатки по объявлениям. Для обновления используйте публичную модель `StockUpdateEntry`. + +```python +from avito import AvitoClient +from avito.orders import StockUpdateEntry + +with AvitoClient.from_env() as avito: + current = avito.stock().get(item_ids=[101]) + updated = avito.stock().update( + stocks=[StockUpdateEntry(item_id=101, quantity=7)], + idempotency_key="stock-update-example-1", + ) + +print(current.items[0].quantity) +print(updated.items[0].success) +``` + +Полный список методов смотрите в [reference по orders](../reference/domains/orders.md). Бинарные ответы, такие как PDF-этикетки, сериализуются через `to_dict()` с `content_base64`. diff --git a/docs/site/how-to/ratings-and-tariffs.md b/docs/site/how-to/ratings-and-tariffs.md new file mode 100644 index 0000000..2197536 --- /dev/null +++ b/docs/site/how-to/ratings-and-tariffs.md @@ -0,0 +1,84 @@ +# Рейтинги, отзывы и тарифы + +Этот рецепт закрывает два близких сценария личного кабинета: контроль рейтинга и отзывов, а также чтение текущего тарифа аккаунта. + +## Рейтинговый профиль + +`rating_profile().get()` возвращает агрегированную информацию по рейтингу. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + rating = avito.rating_profile().get() + +print(rating.is_enabled) +print(rating.score) +print(rating.reviews_count) +``` + +## Список отзывов + +Отзывы читаются через `review().list()`. Для перехода по страницам используйте `ReviewsQuery`. + +```python +from avito import AvitoClient +from avito.ratings.models import ReviewsQuery + +with AvitoClient.from_env() as avito: + reviews = avito.review().list(query=ReviewsQuery(page=1)) + +print(reviews.items[0].review_id) +print(reviews.items[0].text) +print(reviews.items[0].can_answer) +``` + +## Ответ на отзыв + +Для создания ответа нужен числовой `review_id` и текст. Для повторяемых write-вызовов передавайте `idempotency_key`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + answer = avito.review_answer().create( + review_id=123, + text="Спасибо за отзыв", + idempotency_key="review-answer-example-1", + ) + +print(answer.answer_id) +print(answer.created_at) +``` + +## Удаление ответа + +`answer_id` можно передать в фабрику `review_answer()` или явно в метод `delete()`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + deleted = avito.review_answer("456").delete( + idempotency_key="review-delete-example-1", + ) + +print(deleted.success) +``` + +## Текущий тариф + +`tariff().get_tariff_info()` возвращает текущий и запланированный тарифные контракты. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + tariff = avito.tariff().get_tariff_info() + +print(tariff.current.level if tariff.current else None) +print(tariff.current.packages_count if tariff.current else None) +print(tariff.scheduled.level if tariff.scheduled else None) +``` + +Полный контракт смотрите в [reference по ratings](../reference/domains/ratings.md) и [reference по tariffs](../reference/domains/tariffs.md). diff --git a/scripts/build_docs_quality_report.py b/scripts/build_docs_quality_report.py index ffa4de4..c198ae7 100644 --- a/scripts/build_docs_quality_report.py +++ b/scripts/build_docs_quality_report.py @@ -58,6 +58,15 @@ def placeholder_count() -> int: return count +def docs_examples_harness_enabled() -> bool: + makefile = (ROOT / "Makefile").read_text(encoding="utf-8") + return ( + "poetry run pytest tests/docs/" in makefile + and (ROOT / "tests" / "docs" / "test_markdown_examples.py").exists() + and (ROOT / "tests" / "docs" / "conftest.py").exists() + ) + + def public_domains() -> list[str]: excluded = {"auth", "core", "testing"} return sorted({row.sdk_package for row in parse_inventory() if row.sdk_package not in excluded}) @@ -114,7 +123,10 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: domains = public_domains() domain_grade = 1.0 if len(domain_coverage) == len(domains) else 0.25 reference_grade = 1.0 if reference_gaps == 0 and docstring_gaps == 0 else 0.5 - example_grade = 0.0 + harness_enabled = docs_examples_harness_enabled() + example_grade = 1.0 if harness_enabled else 0.0 + explanation_target = 10 + explanation_grade = 1.0 if len(explanations) >= explanation_target else 0.25 return { "generated_at": datetime.now(UTC).isoformat(), @@ -139,9 +151,17 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: reference_grade, f"reference-public gaps={reference_gaps}; docstring gaps={docstring_gaps}", ), - "15.4": grade(0.25, f"explanations pages={len(explanations)}"), + "15.4": grade( + explanation_grade, + f"explanations pages={len(explanations)} из {explanation_target}", + ), "15.5": grade(0.5, "CHANGELOG подключён в docs/site/changelog.md"), - "15.6": grade(example_grade, "mktestdocs harness ещё не включён"), + "15.6": grade( + example_grade, + "pytest tests/docs/ включён в docs-strict" + if harness_enabled + else "docs examples harness ещё не включён", + ), }, "supporting_gates": { "7.3_debug_info_safe_by_default": grade(0.5, "debug_info есть в client.md"), diff --git a/tests/docs/conftest.py b/tests/docs/conftest.py index ce11a68..fa38579 100644 --- a/tests/docs/conftest.py +++ b/tests/docs/conftest.py @@ -5,7 +5,7 @@ import pytest from avito import AvitoClient -from avito.testing import FakeTransport +from avito.testing import FakeResponse, FakeTransport def build_docs_client() -> AvitoClient: @@ -15,6 +15,442 @@ def build_docs_client() -> AvitoClient: "/core/v1/accounts/self", {"id": 7, "name": "Иван", "email": "user@example.com", "phone": "+7999"}, ) + fake.add_json( + "GET", + "/core/v1/accounts/7/balance/", + {"user_id": 7, "balance": {"real": 1500.0, "bonus": 250.0, "currency": "RUB"}}, + ) + fake.add_json( + "POST", + "/core/v1/accounts/operations_history/", + { + "total": 1, + "operations": [ + { + "id": "op-1", + "created_at": "2026-04-01T12:00:00Z", + "amount": -300.0, + "type": "payment", + "status": "done", + "description": "Оплата продвижения", + } + ], + }, + ) + fake.add_json( + "GET", + "/checkAhUserV1", + {"user_id": 7, "is_active": True, "role": "manager"}, + ) + fake.add_json( + "GET", + "/getEmployeesV1", + { + "employees": [ + {"employee_id": 10, "user_id": 7, "name": "Пётр", "email": "petr@example.com"} + ], + "total": 1, + }, + ) + fake.add_json( + "GET", + "/listCompanyPhonesV1", + {"phones": [{"id": 1, "phone": "+7000", "comment": "Основной"}]}, + ) + fake.add_json( + "POST", + "/listItemsByEmployeeIdV1", + { + "items": [{"item_id": 101, "title": "Диван", "status": "active", "price": 9900}], + "total": 1, + }, + ) + fake.add_json( + "POST", + "/linkItemsV1", + {"success": True, "message": "linked"}, + ) + fake.add_json( + "GET", + "/core/v1/items", + { + "items": [ + { + "id": 101, + "user_id": 7, + "title": "Диван", + "description": "Угловой диван", + "status": "active", + "price": 9900, + "url": "https://www.avito.ru/items/101", + }, + { + "id": 102, + "user_id": 7, + "title": "Кресло", + "status": "active", + "price": 3500, + }, + ], + "total": 2, + }, + ) + fake.add_json( + "GET", + "/core/v1/accounts/7/items/101/", + { + "id": 101, + "user_id": 7, + "title": "Диван", + "description": "Угловой диван", + "status": "active", + "price": 9900, + "url": "https://www.avito.ru/items/101", + }, + ) + fake.add_json( + "POST", + "/stats/v1/accounts/7/items", + {"items": [{"item_id": 101, "views": 45, "contacts": 5, "favorites": 2}]}, + ) + fake.add_json( + "POST", + "/stats/v2/accounts/7/items", + { + "period": "day", + "items": [{"item_id": 101, "views": 45, "contacts": 5, "favorites": 2}], + }, + ) + fake.add_json( + "POST", + "/core/v1/accounts/7/calls/stats/", + {"items": [{"item_id": 101, "calls": 3, "answered_calls": 2, "missed_calls": 1}]}, + ) + fake.add_json( + "POST", + "/stats/v2/accounts/7/spendings", + {"items": [{"item_id": 101, "amount": 77.5, "service": "xl"}], "total": 77.5}, + ) + fake.add_json( + "POST", + "/core/v1/accounts/7/vas/prices", + {"items": [{"code": "xl", "title": "XL", "price": 500, "is_available": True}]}, + ) + fake.add_json( + "POST", + "/core/v1/items/101/update_price", + {"item_id": 101, "price": 10900, "status": "updated"}, + ) + fake.add_json( + "GET", + "/messenger/v2/accounts/7/chats", + { + "chats": [ + { + "id": "chat-1", + "user_id": 7, + "title": "Покупатель", + "unread_count": 1, + "last_message": {"text": "Здравствуйте"}, + } + ], + "total": 1, + }, + ) + fake.add_json( + "GET", + "/messenger/v2/accounts/7/chats/chat-1", + { + "id": "chat-1", + "user_id": 7, + "title": "Покупатель", + "unread_count": 1, + "last_message": {"text": "Здравствуйте"}, + }, + ) + fake.add_json( + "GET", + "/messenger/v3/accounts/7/chats/chat-1/messages/", + { + "messages": [ + { + "id": "msg-0", + "chat_id": "chat-1", + "author_id": 100, + "text": "Здравствуйте", + "created_at": "2026-04-23T10:00:00Z", + "direction": "in", + "type": "text", + } + ], + "total": 1, + }, + ) + fake.add_json( + "POST", + "/messenger/v1/accounts/7/chats/chat-1/messages", + {"success": True, "message_id": "msg-1", "status": "sent"}, + ) + fake.add_json( + "POST", + "/messenger/v1/accounts/7/uploadImages", + {"images": [{"image_id": "img-1", "url": "https://cdn.example/img-1.jpg"}]}, + ) + fake.add_json( + "POST", + "/messenger/v1/accounts/7/chats/chat-1/messages/image", + {"success": True, "message_id": "msg-img-1", "status": "sent"}, + ) + fake.add_json( + "POST", + "/messenger/v1/accounts/7/chats/chat-1/read", + {"success": True, "status": "read"}, + ) + fake.add_json( + "GET", + "/order-management/1/orders", + { + "orders": [ + { + "id": "ord-1", + "status": "new", + "created": "2026-04-23T09:00:00Z", + "buyerInfo": {"fullName": "Иван"}, + "totalPrice": 9900, + } + ], + "total": 1, + }, + ) + fake.add_json( + "POST", + "/order-management/1/order/applyTransition", + {"result": {"success": True, "orderId": "ord-1", "status": "confirmed"}}, + ) + fake.add_json( + "POST", + "/order-management/1/markings", + {"result": {"success": True, "orderId": "ord-1", "status": "marked"}}, + ) + fake.add_json( + "POST", + "/order-management/1/orders/labels", + {"result": {"taskId": 42, "status": "created"}}, + ) + fake.add( + "GET", + "/order-management/1/orders/labels/42/download", + FakeResponse( + 200, + content=b"%PDF-1.4 docs-label", + headers={ + "content-type": "application/pdf", + "content-disposition": 'attachment; filename="label-42.pdf"', + }, + ), + ) + fake.add_json( + "POST", + "/createAnnouncement", + {"data": {"taskId": 11, "status": "announcement-created"}}, + ) + fake.add_json( + "POST", + "/createParcel", + {"data": {"parcelId": "par-1", "status": "parcel-created"}}, + ) + fake.add_json( + "GET", + "/delivery-sandbox/tasks/11", + {"data": {"taskId": 11, "status": "done"}}, + ) + fake.add_json( + "POST", + "/stock-management/1/info", + { + "stocks": [ + { + "item_id": 101, + "quantity": 5, + "is_multiple": True, + "is_unlimited": False, + "is_out_of_stock": False, + } + ] + }, + ) + fake.add_json( + "PUT", + "/stock-management/1/stocks", + {"stocks": [{"item_id": 101, "external_id": "SKU-101", "success": True, "errors": []}]}, + ) + fake.add_json( + "GET", + "/job/v1/applications/get_ids", + { + "items": [{"id": "app-1", "updatedAt": "2026-04-23T10:00:00+03:00"}], + "cursor": "app-1", + }, + ) + fake.add_json( + "POST", + "/job/v1/applications/get_by_ids", + { + "applies": [ + { + "id": "app-1", + "vacancy_id": 101, + "resume_id": "res-1", + "state": "new", + "is_viewed": False, + "applicant": {"name": "Иван"}, + } + ] + }, + ) + fake.add_json( + "GET", + "/job/v1/applications/get_states", + {"states": [{"slug": "new", "description": "Новый отклик"}]}, + ) + fake.add_json( + "POST", + "/job/v1/applications/set_is_viewed", + {"ok": True, "status": "viewed"}, + ) + fake.add_json( + "POST", + "/job/v1/applications/apply_actions", + {"ok": True, "status": "invited"}, + ) + fake.add_json( + "GET", + "/job/v2/vacancies", + {"vacancies": [{"id": 101, "uuid": "vac-uuid-1", "title": "Продавец", "status": "active"}], "total": 1}, + ) + fake.add_json( + "GET", + "/job/v2/vacancies/101", + { + "id": 101, + "uuid": "vac-uuid-1", + "title": "Продавец", + "status": "active", + "url": "https://avito.ru/vacancy/101", + }, + ) + fake.add_json( + "GET", + "/job/v1/resumes/", + { + "meta": {"cursor": "2", "total": 1}, + "resumes": [ + { + "id": "res-1", + "title": "Оператор call-центра", + "name": "Пётр", + "location": "Москва", + "salary": 90000, + } + ], + }, + ) + fake.add_json( + "GET", + "/job/v2/resumes/res-1", + { + "id": "res-1", + "title": "Оператор call-центра", + "fullName": "Пётр Петров", + "address_details": {"location": "Москва"}, + "salary": {"from": 90000}, + }, + ) + fake.add_json( + "GET", + "/job/v1/resumes/res-1/contacts/", + {"name": "Пётр", "phone": "+79990000000", "email": "petr@example.com"}, + ) + fake.add_json( + "GET", + "/job/v1/applications/webhook", + {"url": "https://example.com/job", "is_active": True, "version": "v1"}, + ) + fake.add_json( + "PUT", + "/job/v1/applications/webhook", + {"url": "https://example.com/job", "is_active": True, "version": "v1"}, + ) + fake.add_json( + "GET", + "/job/v2/vacancy/dict", + [{"id": "profession", "description": "Профессия"}], + ) + fake.add_json( + "GET", + "/job/v2/vacancy/dict/profession", + [{"id": 10106, "name": "IT, интернет, телеком", "deprecated": False}], + ) + fake.add_json( + "GET", + "/ratings/v1/info", + { + "isEnabled": True, + "rating": {"score": 4.7, "reviewsCount": 25, "reviewsWithScoreCount": 20}, + }, + ) + fake.add_json( + "GET", + "/ratings/v1/reviews", + { + "total": 25, + "reviews": [ + { + "id": 123, + "score": 5, + "stage": "done", + "text": "Все отлично", + "createdAt": 1713427200, + "canAnswer": True, + "usedInScore": True, + } + ], + }, + ) + fake.add_json( + "POST", + "/ratings/v1/answers", + {"id": 456, "createdAt": 1713427200}, + ) + fake.add_json( + "DELETE", + "/ratings/v1/answers/456", + {"success": True}, + ) + fake.add_json( + "GET", + "/tariff/info/1", + { + "current": { + "level": "Тариф Максимальный", + "isActive": True, + "startTime": 1713427200, + "closeTime": 1716029200, + "bonus": 10, + "packages": [{"id": 1}, {"id": 2}], + "price": {"price": 1990, "originalPrice": 2490}, + }, + "scheduled": { + "level": "Тариф Базовый", + "isActive": False, + "startTime": 1716029300, + "closeTime": None, + "bonus": 0, + "packages": [], + "price": {"price": 990, "originalPrice": 990}, + }, + }, + ) return fake.as_client(user_id=7) From f389f58276dad634a866c68d2365d9bcef037739 Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Fri, 24 Apr 2026 01:38:18 +0300 Subject: [PATCH 6/9] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 49 ++- docs/site/explanations/.pages | 9 + .../api-coverage-and-deprecations.md | 28 ++ docs/site/explanations/auth-flow.md | 39 +++ docs/site/explanations/config-resolution.md | 26 ++ .../explanations/dry-run-and-idempotency.md | 23 ++ docs/site/explanations/error-model.md | 31 ++ docs/site/explanations/index.md | 15 +- .../site/explanations/pagination-semantics.md | 35 +++ .../explanations/security-and-redaction.md | 23 ++ docs/site/explanations/testing-strategy.md | 25 ++ .../explanations/transport-and-retries.md | 34 +++ docs/site/how-to/.pages | 4 + docs/site/how-to/autoteka-report.md | 71 +++++ docs/site/how-to/cpa-calltracking.md | 50 +++ docs/site/how-to/index.md | 8 +- docs/site/how-to/promotion-dry-run.md | 66 ++++ docs/site/how-to/realty-booking.md | 55 ++++ poetry.lock | 17 +- pyproject.toml | 1 + tests/docs/conftest.py | 286 ++++++++++++++++++ tests/docs/test_markdown_examples.py | 31 +- 22 files changed, 872 insertions(+), 54 deletions(-) create mode 100644 docs/site/explanations/api-coverage-and-deprecations.md create mode 100644 docs/site/explanations/auth-flow.md create mode 100644 docs/site/explanations/config-resolution.md create mode 100644 docs/site/explanations/dry-run-and-idempotency.md create mode 100644 docs/site/explanations/error-model.md create mode 100644 docs/site/explanations/pagination-semantics.md create mode 100644 docs/site/explanations/security-and-redaction.md create mode 100644 docs/site/explanations/testing-strategy.md create mode 100644 docs/site/explanations/transport-and-retries.md create mode 100644 docs/site/how-to/autoteka-report.md create mode 100644 docs/site/how-to/cpa-calltracking.md create mode 100644 docs/site/how-to/promotion-dry-run.md create mode 100644 docs/site/how-to/realty-booking.md diff --git a/README.md b/README.md index 5178886..09ddbbe 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ with AvitoClient.from_env() as avito: account = avito.account(user_id=123) balance = account.get_balance() ad = avito.ad(item_id=42, user_id=123).get() - stats = avito.ad_stats(item_id=42, user_id=123).get() + stats = avito.ad_stats(item_id=42, user_id=123).get_item_stats() ``` ### Автозагрузка @@ -176,8 +176,8 @@ with AvitoClient.from_env() as avito: forecast = avito.bbip_promotion(item_id=42).get_forecasts(items=[]) budget = avito.autostrategy_campaign().create_budget( campaign_type="AS", - start_time="2026-04-20T00:00:00Z", - finish_time="2026-04-27T00:00:00Z", + start_time=datetime(2026, 4, 20), + finish_time=datetime(2026, 4, 27), items=[42, 43], ) campaign = avito.autostrategy_campaign(campaign_id=15).get() @@ -200,13 +200,12 @@ Write-операции продвижения, поддерживающие су ```python from avito import AvitoClient -from avito.orders import OrderLabelsRequest, StockInfoRequest with AvitoClient.from_env() as avito: orders = avito.order().list() - label_task = avito.order_label().create(request=OrderLabelsRequest(order_ids=["100500"])) + label_task = avito.order_label().create(order_ids=["100500"]) label_pdf = avito.order_label(task_id=label_task.task_id).download() - stock_info = avito.stock().get(request=StockInfoRequest(item_ids=[100500])) + stock_info = avito.stock().get(item_ids=[100500]) ``` ### Работа @@ -228,14 +227,11 @@ with AvitoClient.from_env() as avito: ```python from avito import AvitoClient -from avito.cpa import CpaCallsByTimeRequest with AvitoClient.from_env() as avito: calls = avito.cpa_call().list( - request=CpaCallsByTimeRequest( - date_time_from="2026-04-18T00:00:00Z", - date_time_to="2026-04-19T00:00:00Z", - ) + date_time_from="2026-04-18T00:00:00Z", + date_time_to="2026-04-19T00:00:00Z", ) calltracking = avito.call_tracking_call(10).get() records = avito.call_tracking_call(10).download() @@ -245,18 +241,11 @@ with AvitoClient.from_env() as avito: ```python from avito import AvitoClient -from avito.autoteka import CatalogResolveRequest, PreviewReportRequest, VinRequest with AvitoClient.from_env() as avito: - catalog = avito.autoteka_vehicle().resolve_catalog( - request=CatalogResolveRequest(brand_id=1) - ) - preview = avito.autoteka_vehicle().create_preview_by_vin( - request=VinRequest(vin="XTA00000000000000") - ) - report = avito.autoteka_report().create_report( - request=PreviewReportRequest(preview_id=int(preview.preview_id or 0)) - ) + catalog = avito.autoteka_vehicle().resolve_catalog(brand_id=1) + preview = avito.autoteka_vehicle().create_preview_by_vin(vin="XTA00000000000000") + report = avito.autoteka_report().create_report(preview_id=int(preview.preview_id or 0)) reports = avito.autoteka_report().list_reports() ``` @@ -264,18 +253,14 @@ with AvitoClient.from_env() as avito: ```python from avito import AvitoClient -from avito.realty import RealtyBookingsUpdateRequest, RealtyPricePeriod, RealtyPricesUpdateRequest +from avito.realty import RealtyPricePeriod with AvitoClient.from_env() as avito: booking = avito.realty_booking(20, user_id=10) - booking.update_bookings_info( - request=RealtyBookingsUpdateRequest(blocked_dates=["2026-05-01"]) - ) + booking.update_bookings_info(blocked_dates=["2026-05-01"]) bookings = booking.list_realty_bookings(date_start="2026-05-01", date_end="2026-05-05") avito.realty_pricing(20, user_id=10).update_realty_prices( - request=RealtyPricesUpdateRequest( - periods=[RealtyPricePeriod(date_from="2026-05-01", price=5000)] - ) + periods=[RealtyPricePeriod(date_from="2026-05-01", price=5000)] ) reviews = avito.review().list() tariff = avito.tariff().get_tariff_info() @@ -300,11 +285,11 @@ with AvitoClient.from_env() as avito: from avito import AvitoClient with AvitoClient.from_env() as avito: - result = avito.ad(user_id=123).list(status="active", limit=50) + items = avito.ad(user_id=123).list(status="active", limit=50) - first = result.items[0] - preview = result.items[:10] - all_items = result.items.materialize() + first = items[0] + preview = items[:10] + all_items = items.materialize() ``` ## Ошибки diff --git a/docs/site/explanations/.pages b/docs/site/explanations/.pages index 3597ffb..ff075d8 100644 --- a/docs/site/explanations/.pages +++ b/docs/site/explanations/.pages @@ -1,3 +1,12 @@ nav: - index.md - architecture.md + - auth-flow.md + - transport-and-retries.md + - error-model.md + - pagination-semantics.md + - dry-run-and-idempotency.md + - testing-strategy.md + - api-coverage-and-deprecations.md + - config-resolution.md + - security-and-redaction.md diff --git a/docs/site/explanations/api-coverage-and-deprecations.md b/docs/site/explanations/api-coverage-and-deprecations.md new file mode 100644 index 0000000..6c82b3f --- /dev/null +++ b/docs/site/explanations/api-coverage-and-deprecations.md @@ -0,0 +1,28 @@ +# Покрытие API и deprecation + +Swagger/OpenAPI-файлы в `docs/avito/api/` считаются upstream source of truth. `docs/avito/inventory.md` связывает каждую HTTP-операцию с доменным объектом SDK, публичным методом, типами запроса/ответа и deprecation metadata. + +```mermaid +flowchart LR + spec[docs/avito/api/*.json] --> sync[check_spec_inventory_sync.py] + inventory[docs/avito/inventory.md] --> sync + inventory --> coverage[check_inventory_coverage.py] + inventory --> reference[Generated reference] + sdk[avito/* public API] --> coverage +``` + +## Почему нужны оба источника + +OpenAPI описывает upstream API. Inventory описывает, где эта операция живёт в SDK. Если операция есть в spec, но отсутствует в inventory, пользователь не найдёт её в SDK. Если операция есть в inventory, но отсутствует в spec, inventory устарел или описывает неподтверждённый контракт. + +## Deprecated metadata + +Для deprecated-операций inventory хранит `deprecated_since`, `replacement` и `removal_version`. Эти поля нужны сразу в трёх местах: runtime `DeprecationWarning`, reference warning и changelog/release notes. + +Deprecated-страница в reference не заменяет runtime warning. Если символ устарел, пользователь должен получить предупреждение при вызове, а не только при чтении сайта. + +## Гейты + +`check_spec_inventory_sync.py` сверяет operation-level coverage: документ, раздел, HTTP method и path. `check_inventory_coverage.py` сверяет связь inventory с публичной SDK-поверхностью и sanity deprecation-полей. + +Страница для пользователя: [покрытие API](../reference/coverage.md). Карта операций: [operations reference](../reference/operations.md). diff --git a/docs/site/explanations/auth-flow.md b/docs/site/explanations/auth-flow.md new file mode 100644 index 0000000..41de3bc --- /dev/null +++ b/docs/site/explanations/auth-flow.md @@ -0,0 +1,39 @@ +# OAuth и токены + +SDK прячет OAuth-обмен за `AuthProvider`, чтобы пользовательский код работал с доменными методами, а не с access token, refresh token и заголовками `Authorization`. + +```mermaid +sequenceDiagram + participant App as Пользовательский код + participant Client as AvitoClient + participant Domain as Домен + participant Transport as Transport + participant Auth as AuthProvider + participant API as Avito API + + App->>Client: AvitoClient.from_env() + App->>Domain: account().get_self() + Domain->>Transport: GET /core/v1/accounts/self + Transport->>Auth: get_access_token() + Auth-->>Transport: access token + Transport->>API: request + Authorization + API-->>Transport: JSON response + Transport-->>Domain: payload + Domain-->>App: typed model +``` + +## Где живёт ответственность + +`AvitoClient` создаёт общий контекст: настройки, auth и transport. Доменный объект выбирает бизнес-операцию. Section client знает HTTP path и payload. `Transport` добавляет токен и применяет retry. `AuthProvider` кэширует токен и обновляет его, если upstream отвечает `401`. + +Такой порядок важен для public contract: публичный метод не принимает access token, не возвращает OAuth-payload и не требует от пользователя повторять refresh-flow. + +## Ошибка 401 + +`401` считается ошибкой аутентификации, а не авторизации. SDK инвалидирует токен там, где это допустимо, и поднимает `AuthenticationError`, если запрос не может быть выполнен успешно. `403` остаётся отдельным `AuthorizationError`: эти типы не наследуются друг от друга. + +## Autoteka + +Часть операций Автотеки использует отдельные OAuth-настройки. Они лежат в `AuthSettings` рядом с основными credentials, но не смешиваются с публичными методами домена: пользователь вызывает `autoteka_vehicle()` и `autoteka_report()`, а выбор token endpoint остаётся внутренним поведением auth-слоя. + +Список env-переменных смотрите в [reference по конфигурации](../reference/config.md). diff --git a/docs/site/explanations/config-resolution.md b/docs/site/explanations/config-resolution.md new file mode 100644 index 0000000..477c9e6 --- /dev/null +++ b/docs/site/explanations/config-resolution.md @@ -0,0 +1,26 @@ +# Resolution конфигурации + +Конфигурация должна быть детерминированной: один и тот же набор env-переменных и `.env` даёт один и тот же `AvitoSettings`. Это важно для CLI, CI и долгоживущих фоновых задач. + +```mermaid +flowchart TD + process[Process environment] --> merge[Resolution] + dotenv[.env file] --> merge + defaults[Defaults] --> merge + merge --> settings[AvitoSettings] + settings --> client[AvitoClient] +``` + +## Приоритет + +Значения из process environment имеют приоритет над `.env`. `.env` имеет приоритет над значениями по умолчанию. Если `env_file` передан явно, SDK читает именно этот файл. Если обязательные OAuth credentials отсутствуют, клиент поднимает `ConfigurationError` при создании, до первого HTTP-запроса. + +## Alias-переменные + +Для OAuth доступны короткие alias-переменные вроде `AVITO_CLIENT_ID` и вложенные имена вроде `AVITO_AUTH__CLIENT_ID`. Alias нужен для удобства, вложенное имя — для явного соответствия `AuthSettings`. + +## Иммутабельность клиента + +После создания `AvitoClient` настройки не меняются. Если нужен другой `base_url`, `user_id`, auth или retry policy, создаётся новый клиент. Это убирает класс ошибок, где часть доменных объектов работает со старой конфигурацией, а часть — с новой. + +Полная таблица env-переменных и per-operation overrides находится в [reference по конфигурации](../reference/config.md). diff --git a/docs/site/explanations/dry-run-and-idempotency.md b/docs/site/explanations/dry-run-and-idempotency.md new file mode 100644 index 0000000..b1dc103 --- /dev/null +++ b/docs/site/explanations/dry-run-and-idempotency.md @@ -0,0 +1,23 @@ +# Dry-run и идемпотентность + +Write-операции в SDK должны быть предсказуемыми: один и тот же пользовательский ввод строит один и тот же payload, а повторный реальный вызов можно защитить `idempotency_key`, если endpoint это поддерживает. + +## Dry-run + +`dry_run=True` не вызывает transport. Метод валидирует аргументы, строит payload и возвращает preview-модель. Это полезно для интерфейсов подтверждения, batch-задач и consumer-тестов. + +Ключевое правило: dry-run не должен строить “похожий” payload. Он обязан использовать тот же путь сериализации, что и реальный вызов. Иначе preview перестанет быть надёжной проверкой. + +## Идемпотентность + +`idempotency_key` нужен для повторяемых write-вызовов: retry после сетевого сбоя, перезапуск фоновой задачи, повторное нажатие пользователем. SDK передаёт ключ в transport-layer только для реального write-вызова; dry-run не должен обращаться в сеть. + +## Разделение поведения + +| Режим | Transport | Payload | Возврат | +|---|---|---|---| +| `dry_run=True` | Не вызывается | Строится полностью | Preview/result model | +| `dry_run=False` без ключа | Вызывается один раз | Строится полностью | Upstream result model | +| `dry_run=False` с ключом | Вызывается с idempotency metadata | Строится полностью | Upstream result model | + +Практический сценарий описан в [how-to по продвижению](../how-to/promotion-dry-run.md). Разрешённые overrides перечислены в [reference по конфигурации](../reference/config.md). diff --git a/docs/site/explanations/error-model.md b/docs/site/explanations/error-model.md new file mode 100644 index 0000000..0893666 --- /dev/null +++ b/docs/site/explanations/error-model.md @@ -0,0 +1,31 @@ +# Модель ошибок + +SDK переводит HTTP-статусы, transport failures и ошибки маппинга в typed exceptions. Пользовательскому коду не нужно разбирать `httpx.Response` или сырой JSON ошибки. + +## Классы ошибок + +| Ситуация | Исключение | +|---|---| +| Некорректный пользовательский ввод | `ValidationError` | +| Не хватает настроек клиента | `ConfigurationError` | +| `401` | `AuthenticationError` | +| `403` | `AuthorizationError` | +| `409` | `ConflictError` | +| `429` | `RateLimitError` | +| Transport failure | `TransportError` | +| Upstream вернул неподдержанный ответ | `UpstreamApiError` | +| JSON не соответствует SDK-модели | `ResponseMappingError` | + +`AuthenticationError` и `AuthorizationError` — разные ветки. Это позволяет consumer-коду отдельно обрабатывать истёкший токен и недостаток прав. + +## Metadata + +Каждая публичная ошибка несёт безопасный диагностический набор: `operation`, `status`, `request_id`, `attempt`, `method`, `endpoint`. Эти поля нужны для логов, алертов и обращений в поддержку upstream API. + +Секреты не должны попадать в сообщения ошибок. Перед сохранением metadata SDK санитайзит OAuth-токены, `client_secret` и `Authorization`. + +## Где ловить ошибки + +Пользовательский код обычно ловит `AvitoError` на границе integration job, CLI-команды или web-handler. Более точные типы нужны, когда поведение реально различается: например, `RateLimitError` можно отправить в delayed retry, а `ValidationError` надо вернуть как ошибку конфигурации задачи. + +Полный список public exceptions и полей metadata смотрите в [reference по исключениям](../reference/exceptions.md). diff --git a/docs/site/explanations/index.md b/docs/site/explanations/index.md index 3073ac7..c4617f9 100644 --- a/docs/site/explanations/index.md +++ b/docs/site/explanations/index.md @@ -5,11 +5,14 @@ Explanations описывают причины архитектурных реш | Статья | Что объясняет | |---|---| | [Архитектура SDK](architecture.md) | Как `AvitoClient`, домены, section clients, transport, auth и mappers разделяют ответственность | -| Transport и retry | Почему retry живёт в transport-слое и как учитываются 429/5xx | -| Модель ошибок | Как HTTP-коды превращаются в typed exceptions | -| Семантика пагинации | Почему `PaginatedList` ленивый и когда загружаются страницы | -| Dry-run и идемпотентность | Как write-операции проверяются без сетевого вызова | -| Security и redaction | Какие секреты SDK не раскрывает в диагностике и ошибках | -| Совместимость | Как deprecation отражается в runtime, reference и changelog | +| [OAuth и токены](auth-flow.md) | Почему token-flow скрыт за `AuthProvider` | +| [Transport и retry](transport-and-retries.md) | Почему retry живёт в transport-слое и как учитываются 429/5xx | +| [Модель ошибок](error-model.md) | Как HTTP-коды превращаются в typed exceptions | +| [Семантика пагинации](pagination-semantics.md) | Почему `PaginatedList` ленивый и когда загружаются страницы | +| [Dry-run и идемпотентность](dry-run-and-idempotency.md) | Как write-операции проверяются без сетевого вызова | +| [Стратегия тестирования](testing-strategy.md) | Как `FakeTransport`, contract-тесты и docs-harness проверяют SDK | +| [Покрытие API и deprecation](api-coverage-and-deprecations.md) | Как spec, inventory, reference и runtime warnings связаны между собой | +| [Resolution конфигурации](config-resolution.md) | Как env, `.env` и defaults превращаются в `AvitoSettings` | +| [Security и redaction](security-and-redaction.md) | Какие секреты SDK не раскрывает в диагностике и ошибках | Для практических сценариев используйте [how-to рецепты](../how-to/index.md), а для полного API-контракта — [reference](../reference/index.md). diff --git a/docs/site/explanations/pagination-semantics.md b/docs/site/explanations/pagination-semantics.md new file mode 100644 index 0000000..a90fb5d --- /dev/null +++ b/docs/site/explanations/pagination-semantics.md @@ -0,0 +1,35 @@ +# Семантика пагинации + +`PaginatedList[T]` делает list-операции ленивыми: первая страница загружается сразу, а последующие страницы читаются только при обращении к данным за пределами уже загруженного диапазона. + +```mermaid +sequenceDiagram + participant App as Код + participant PageList as PaginatedList + participant Paginator as Paginator + participant API as API + + App->>Paginator: list(limit=50) + Paginator->>API: page 1 + API-->>Paginator: first page + Paginator-->>App: PaginatedList + App->>PageList: items[0] + PageList-->>App: cached item + App->>PageList: materialize() + PageList->>API: next pages + API-->>PageList: remaining items +``` + +## Почему не обычный `list` + +Обычный `list[T]` заставил бы SDK либо загружать все страницы сразу, либо скрывать сетевые запросы внутри уже материализованного типа. `PaginatedList[T]` явно показывает, что чтение коллекции может вызвать дополнительные запросы. + +## Гарантии + +Первая страница загружается при создании результата. Повторный доступ к уже загруженным элементам не делает новый HTTP-запрос. `materialize()` дочитывает все страницы ровно один раз. Ошибка на следующей странице поднимается в момент обращения к этой странице, а не при создании первой. + +## Что документировать в public API + +Если метод возвращает `PaginatedList[T]` runtime, его аннотация тоже должна быть `PaginatedList[T]`. Аннотация `list[T]` для ленивого результата считается нарушением контракта: пользователь не увидит, где возможны сетевые запросы. + +Практический пример есть в [how-to по объявлениям](../how-to/ad-listing-and-stats.md), полный контракт — в [reference по пагинации](../reference/pagination.md). diff --git a/docs/site/explanations/security-and-redaction.md b/docs/site/explanations/security-and-redaction.md new file mode 100644 index 0000000..0742d37 --- /dev/null +++ b/docs/site/explanations/security-and-redaction.md @@ -0,0 +1,23 @@ +# Security и redaction + +SDK не является secret manager, но обязан не ухудшать безопасность consumer-приложения: не печатать OAuth-секреты в исключениях, не возвращать access token из diagnostics и не смешивать безопасный debug snapshot с transport internals. + +## Что SDK редактирует + +Секретные значения редактируются в error metadata и diagnostic paths: access token, refresh token, `client_secret`, заголовок `Authorization` и близкие OAuth-поля. Это применяется к логируемым metadata и исключениям, чтобы стандартный catch/log `AvitoError` не раскрывал credentials. + +`debug_info()` возвращает только безопасный снимок: `base_url`, `user_id`, флаг auth, таймауты и retry-настройки. В нём нет access token, `client_secret` и raw headers. + +## Что остаётся ответственностью consumer-кода + +Модели SDK могут содержать пользовательские данные: телефоны, email, тексты сообщений, адреса, цены, идентификаторы заказов. `to_dict()` и `model_dump()` сериализуют публичную модель, а не применяют бизнес-редакцию персональных данных. Если consumer-код пишет эти данные в логи, он должен применять собственную политику redaction. + +## Ошибки + +Ошибки SDK сохраняют поля `operation`, `status`, `request_id`, `attempt`, `method`, `endpoint`. Эти поля достаточны для диагностики большинства интеграционных сбоев и не требуют раскрывать raw request body или OAuth headers. + +## Практическое правило + +Логируйте typed exception metadata и `debug_info()`. Не логируйте raw payload, binary content и полные `to_dict()` пользовательских моделей без фильтрации на стороне приложения. + +Поля `debug_info()` описаны в [reference по клиенту](../reference/client.md), а metadata ошибок — в [reference по исключениям](../reference/exceptions.md). diff --git a/docs/site/explanations/testing-strategy.md b/docs/site/explanations/testing-strategy.md new file mode 100644 index 0000000..b89ddd8 --- /dev/null +++ b/docs/site/explanations/testing-strategy.md @@ -0,0 +1,25 @@ +# Стратегия тестирования + +SDK тестируется через публичные контракты: доменные методы, typed-модели, exceptions и `avito.testing`. Тесты не должны подменять приватные поля клиента или зависеть от настоящего HTTP там, где сценарий можно проверить fake transport. + +## Уровни + +| Уровень | Что проверяет | +|---|---| +| Unit | Мапперы, сериализация моделей, validation | +| Contract | Публичная поверхность, исключения, deprecated warnings | +| Domain | Доменные методы поверх `FakeTransport` | +| Docs | README/tutorials/how-to snippets через `mktestdocs` | +| Build gates | Inventory/spec sync, reference surface, docstring contract | + +## FakeTransport + +`FakeTransport` сценарно описывает HTTP-ответы и записывает запросы. Это публичный testing namespace, поэтому consumer-код может строить тесты без реального Avito API, без OAuth-секретов и без monkeypatch приватных атрибутов. + +Docs-harness использует тот же подход: `AvitoClient.from_env()` в markdown-примерах возвращает клиент поверх fake transport, поэтому copy-paste snippets проходят в CI без сетевых запросов. + +## Почему не мокать domain methods + +Если тест подменяет `account().get_self()` напрямую, он проверяет только consumer-код. Если тест строит `AvitoClient` поверх fake transport, он дополнительно проверяет HTTP path, payload, mapper и публичную модель. Поэтому fake transport ближе к реальному интеграционному контракту. + +Практический reference по testing API смотрите в [reference по тестированию](../reference/testing.md). diff --git a/docs/site/explanations/transport-and-retries.md b/docs/site/explanations/transport-and-retries.md new file mode 100644 index 0000000..0bc4e70 --- /dev/null +++ b/docs/site/explanations/transport-and-retries.md @@ -0,0 +1,34 @@ +# Transport и retry + +`Transport` — единственный слой, который работает с `httpx`, таймаутами, retry и mapping HTTP-ошибок. Домены и section clients не повторяют эту логику, иначе публичное поведение разных разделов начало бы расходиться. + +```mermaid +flowchart TD + call[SectionClient request] --> auth{Нужен токен?} + auth -- да --> token[AuthProvider] + auth -- нет --> send[httpx request] + token --> send + send --> status{Ответ} + status -- 2xx --> map[JSON или binary mapper] + status -- 401 --> refresh[Инвалидация токена] + refresh --> retry401{Можно повторить?} + retry401 -- да --> token + retry401 -- нет --> error[Typed exception] + status -- 429/5xx --> retry{Retry допустим?} + retry -- да --> wait[Backoff / Retry-After] + wait --> send + retry -- нет --> error + status -- 4xx --> error +``` + +## Что повторяется + +Retry применяется только там, где операция помечена как безопасная для повтора. Read/list/probe операции обычно допускают retry. Write-операции получают retry только при явной идемпотентности, например через `idempotency_key`, или когда конкретный section client помечает операцию как безопасную. + +`429` учитывает `Retry-After`, если upstream его вернул. Для `5xx` используется retry-политика transport-слоя. Ошибки маппинга не повторяются: если JSON уже получен, но не соответствует контракту модели, это `ResponseMappingError`, а не сетевой сбой. + +## Почему retry не в доменах + +Доменный объект должен описывать публичный сценарий: `order_label().create()` или `ad_stats().get_item_stats()`. Если retry появится на этом уровне, одинаковые HTTP-коды начнут вести себя по-разному в разных пакетах. Поэтому retry централизован и проверяется через transport/fake transport. + +Подробные исключения смотрите в [модели ошибок](error-model.md) и [reference по исключениям](../reference/exceptions.md). diff --git a/docs/site/how-to/.pages b/docs/site/how-to/.pages index 8c118cc..7eab4f7 100644 --- a/docs/site/how-to/.pages +++ b/docs/site/how-to/.pages @@ -3,7 +3,11 @@ nav: - auth-and-config.md - account-profile.md - ad-listing-and-stats.md + - promotion-dry-run.md - chat-image-upload.md - order-labels.md - job-applications.md + - autoteka-report.md + - realty-booking.md + - cpa-calltracking.md - ratings-and-tariffs.md diff --git a/docs/site/how-to/autoteka-report.md b/docs/site/how-to/autoteka-report.md new file mode 100644 index 0000000..964ebec --- /dev/null +++ b/docs/site/how-to/autoteka-report.md @@ -0,0 +1,71 @@ +# Отчёт Автотеки + +Этот рецепт показывает базовую цепочку домена `autoteka`: уточнить параметры каталога, создать preview по VIN, выпустить отчёт и прочитать список готовых отчётов. + +## Каталог + +`resolve_catalog()` помогает получить доступные поля автокаталога для выбранной марки. Метод принимает конкретный `brand_id` и возвращает типизированные поля. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + catalog = avito.autoteka_vehicle().resolve_catalog(brand_id=1) + +print(catalog.items[0].field_id) +print(catalog.items[0].label) +``` + +## Preview по VIN + +Перед выпуском отчёта создайте preview. Для повторяемых запусков передавайте `idempotency_key`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + preview = avito.autoteka_vehicle().create_preview_by_vin( + vin="XTA00000000000000", + idempotency_key="autoteka-preview-example-1", + ) + +print(preview.preview_id) +print(preview.vehicle_id) +``` + +## Выпуск отчёта + +Отчёт создаётся по `preview_id`. Если `preview_id` пришёл строкой, приведите его к `int` перед вызовом. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + preview = avito.autoteka_vehicle().create_preview_by_vin( + vin="XTA00000000000000", + idempotency_key="autoteka-preview-example-2", + ) + report = avito.autoteka_report().create_report( + preview_id=int(preview.preview_id or 0), + idempotency_key="autoteka-report-example-1", + ) + +print(report.report_id) +print(report.status) +``` + +## Список отчётов + +`list_reports()` возвращает готовые отчёты аккаунта Автотеки. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + reports = avito.autoteka_report().list_reports() + +print(reports.items[0].report_id) +print(reports.items[0].vehicle_id) +``` + +Полный контракт смотрите в [reference по autoteka](../reference/domains/autoteka.md). Для подробной карты HTTP-операций используйте [reference операций](../reference/operations.md). diff --git a/docs/site/how-to/cpa-calltracking.md b/docs/site/how-to/cpa-calltracking.md new file mode 100644 index 0000000..089a4e2 --- /dev/null +++ b/docs/site/how-to/cpa-calltracking.md @@ -0,0 +1,50 @@ +# CPA и CallTracking + +Этот рецепт показывает, как читать CPA-звонки за период, открыть конкретный звонок CallTracking и скачать запись разговора без доступа к внутреннему transport. + +## CPA-звонки за период + +`cpa_call().list()` принимает границы периода строками в формате upstream API и возвращает типизированный список звонков. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + calls = avito.cpa_call().list( + date_time_from="2026-04-18T00:00:00Z", + date_time_to="2026-04-19T00:00:00Z", + ) + +print(calls.items[0].call_id) +print(calls.items[0].buyer_phone) +``` + +## Звонок CallTracking + +Если известен `call_id`, используйте `call_tracking_call(call_id).get()`. Идентификатор можно передать в фабрику или явно в метод. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + call = avito.call_tracking_call(10).get() + +print(call.call.call_id if call.call else None) +print(call.call.talk_duration if call.call else None) +``` + +## Запись разговора + +`download()` возвращает бинарную модель записи. Для логов и JSON-отчётов используйте `to_dict()`: бинарное содержимое сериализуется безопасно. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + record = avito.call_tracking_call(10).download() + payload = record.to_dict() + +print(payload["content_base64"]) +``` + +Полный контракт смотрите в [reference по cpa](../reference/domains/cpa.md). Исключения и поля metadata описаны в [reference по ошибкам](../reference/exceptions.md). diff --git a/docs/site/how-to/index.md b/docs/site/how-to/index.md index 9ad66a6..5ed278f 100644 --- a/docs/site/how-to/index.md +++ b/docs/site/how-to/index.md @@ -7,14 +7,14 @@ How-to раздел собирает рецепты для конкретных | [Авторизация и конфигурация](auth-and-config.md) | Создать клиент через env, явные ключи или `AvitoSettings` | | [Профиль, баланс и иерархия аккаунта](account-profile.md) | Получить профиль, баланс, историю операций и данные сотрудников | | [Объявления, статистика и продвижение](ad-listing-and-stats.md) | Найти объявления, открыть карточку, прочитать статистику и подготовить VAS | +| [Продвижение с dry-run](promotion-dry-run.md) | Проверить payload write-операции без сетевого вызова | | [Чаты, сообщения и изображения](chat-image-upload.md) | Отправить текст, загрузить изображение и отправить его в чат | -| Продвижение с dry-run | Проверить payload write-операции без сетевого вызова | | Пагинация | Читать `PaginatedList` частями и материализовать весь список | | [Заказы, этикетки и остатки](order-labels.md) | Создать задачу генерации, скачать PDF и обновить остатки | | [Вакансии, отклики и резюме](job-applications.md) | Получить отклики, обработать их и настроить webhook | -| Отчёт Автотеки | Пройти цепочку VIN → preview → report | -| Бронирование недвижимости | Обновить блокировки дат и цены | -| CPA и CallTracking | Найти звонки и скачать запись | +| [Отчёт Автотеки](autoteka-report.md) | Пройти цепочку VIN → preview → report | +| [Бронирование недвижимости](realty-booking.md) | Обновить блокировки дат и цены | +| [CPA и CallTracking](cpa-calltracking.md) | Найти звонки и скачать запись | | [Рейтинги, отзывы и тарифы](ratings-and-tariffs.md) | Получить отзывы, ответить на отзыв и прочитать тариф | | Тестирование с FakeTransport | Проверить consumer-код без реального HTTP | diff --git a/docs/site/how-to/promotion-dry-run.md b/docs/site/how-to/promotion-dry-run.md new file mode 100644 index 0000000..12f2dec --- /dev/null +++ b/docs/site/how-to/promotion-dry-run.md @@ -0,0 +1,66 @@ +# Продвижение с dry-run + +Этот рецепт показывает безопасный цикл для продвижения: сначала прочитать доступные цены, затем проверить write-payload через `dry_run=True` и только после этого выполнять реальный вызов с `idempotency_key`. + +## Цены услуг + +`ad_promotion().get_vas_prices()` читает доступные услуги продвижения по объявлениям. Для чтения нужен `user_id`, а список объявлений передаётся явно. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + prices = avito.ad_promotion(item_id=101, user_id=7).get_vas_prices(item_ids=[101]) + +print(prices.items[0].code) +print(prices.items[0].price) +``` + +## Проверка payload без transport + +При `dry_run=True` SDK строит тот же payload, что и для реального write-вызова, но transport не вызывается. Это удобно для UI-preview, ревью операций и тестов. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + preview = avito.ad_promotion(item_id=101, user_id=7).apply_vas( + codes=["xl"], + dry_run=True, + ) + +print(preview.action) +print(preview.applied) +print(preview.request_payload) +``` + +## Пакет услуг + +Пакетная операция поддерживает тот же dry-run контракт. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + preview = avito.ad_promotion(item_id=101, user_id=7).apply_vas_package( + package_code="package-xl", + dry_run=True, + ) + +print(preview.status) +print(preview.target["item_id"]) +``` + +## Реальный write-вызов + +Когда preview подтверждён пользователем, выполните реальный вызов с идемпотентным ключом. В этом примере блок не помечен как `python`, потому что он намеренно показывает сетевую write-операцию. + +```text +with AvitoClient.from_env() as avito: + result = avito.ad_promotion(item_id=101, user_id=7).apply_vas( + codes=["xl"], + idempotency_key="promotion-apply-vas-2026-04-23-101", + ) +``` + +Полный контракт смотрите в [reference по ads](../reference/domains/ads.md) и [reference по promotion](../reference/domains/promotion.md). Общие правила идемпотентности и dry-run описаны в [конфигурации](../reference/config.md). diff --git a/docs/site/how-to/realty-booking.md b/docs/site/how-to/realty-booking.md new file mode 100644 index 0000000..af676ea --- /dev/null +++ b/docs/site/how-to/realty-booking.md @@ -0,0 +1,55 @@ +# Бронирование недвижимости + +Этот рецепт показывает основной цикл домена `realty`: заблокировать даты, прочитать бронирования и обновить цены краткосрочной аренды. + +## Блокировка дат + +`realty_booking()` требует `item_id` и `user_id`. Даты передаются списком строк в формате upstream API. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + result = avito.realty_booking(20, user_id=10).update_bookings_info( + blocked_dates=["2026-05-01"], + ) + +print(result.success) +print(result.status) +``` + +## Список бронирований + +Для чтения бронирований задайте границы периода. Если нужны неоплаченные бронирования, передайте `with_unpaid=True`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + bookings = avito.realty_booking(20, user_id=10).list_realty_bookings( + date_start="2026-05-01", + date_end="2026-05-05", + with_unpaid=True, + ) + +print(bookings.items[0].booking_id) +print(bookings.items[0].check_in) +``` + +## Обновление цен + +Для цен используйте публичную модель `RealtyPricePeriod`. + +```python +from avito import AvitoClient +from avito.realty import RealtyPricePeriod + +with AvitoClient.from_env() as avito: + updated = avito.realty_pricing(20, user_id=10).update_realty_prices( + periods=[RealtyPricePeriod(date_from="2026-05-01", price=5000)], + ) + +print(updated.success) +``` + +Полный контракт смотрите в [reference по realty](../reference/domains/realty.md). Ошибки валидации входных параметров описаны в [reference по исключениям](../reference/exceptions.md). diff --git a/poetry.lock b/poetry.lock index dba9198..d1c20d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -970,6 +970,21 @@ griffelib = ">=2.0" mkdocs-autorefs = ">=1.4" mkdocstrings = ">=0.30" +[[package]] +name = "mktestdocs" +version = "0.2.5" +description = "A tool for testing markdown documentation" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "mktestdocs-0.2.5-py3-none-any.whl", hash = "sha256:2b20d2387b2025597f3129773edef16d09b08b1b6d31da6cc51af5b693c10430"}, + {file = "mktestdocs-0.2.5.tar.gz", hash = "sha256:7935d4e665b34b690cdf14749abb842723652485f67af12dce51099bcb98e105"}, +] + +[package.extras] +test = ["pytest"] + [[package]] name = "mypy" version = "1.20.1" @@ -1537,4 +1552,4 @@ bracex = ">=2.1.1" [metadata] lock-version = "2.1" python-versions = "^3.14" -content-hash = "f4e7dec6bc39ba9ed8c323a1cda116dba5d8b00ca9648a670e5fe3ce1382b798" +content-hash = "a96e13b2eced596d3c3cc912c1fde282cac5cb510c9dfa0b71e5d2b37bda2b84" diff --git a/pyproject.toml b/pyproject.toml index 087e176..8de85bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ mike = "^2.1" mkdocstrings = { version = ">=0.27", extras = ["python"] } mkdocs-gen-files = ">=0.5" mkdocs-literate-nav = ">=0.6" +mktestdocs = "^0.2.5" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/tests/docs/conftest.py b/tests/docs/conftest.py index fa38579..8e85670 100644 --- a/tests/docs/conftest.py +++ b/tests/docs/conftest.py @@ -20,6 +20,11 @@ def build_docs_client() -> AvitoClient: "/core/v1/accounts/7/balance/", {"user_id": 7, "balance": {"real": 1500.0, "bonus": 250.0, "currency": "RUB"}}, ) + fake.add_json( + "GET", + "/core/v1/accounts/123/balance/", + {"user_id": 123, "balance": {"real": 1500.0, "bonus": 250.0, "currency": "RUB"}}, + ) fake.add_json( "POST", "/core/v1/accounts/operations_history/", @@ -108,11 +113,29 @@ def build_docs_client() -> AvitoClient: "url": "https://www.avito.ru/items/101", }, ) + fake.add_json( + "GET", + "/core/v1/accounts/123/items/42/", + { + "id": 42, + "user_id": 123, + "title": "Стол", + "description": "Письменный стол", + "status": "active", + "price": 4900, + "url": "https://www.avito.ru/items/42", + }, + ) fake.add_json( "POST", "/stats/v1/accounts/7/items", {"items": [{"item_id": 101, "views": 45, "contacts": 5, "favorites": 2}]}, ) + fake.add_json( + "POST", + "/stats/v1/accounts/123/items", + {"items": [{"item_id": 42, "views": 45, "contacts": 5, "favorites": 2}]}, + ) fake.add_json( "POST", "/stats/v2/accounts/7/items", @@ -121,6 +144,14 @@ def build_docs_client() -> AvitoClient: "items": [{"item_id": 101, "views": 45, "contacts": 5, "favorites": 2}], }, ) + fake.add_json( + "POST", + "/stats/v2/accounts/123/items", + { + "period": "day", + "items": [{"item_id": 42, "views": 45, "contacts": 5, "favorites": 2}], + }, + ) fake.add_json( "POST", "/core/v1/accounts/7/calls/stats/", @@ -141,6 +172,212 @@ def build_docs_client() -> AvitoClient: "/core/v1/items/101/update_price", {"item_id": 101, "price": 10900, "status": "updated"}, ) + fake.add_json( + "POST", + "/cpa/v2/callsByTime", + { + "calls": [ + { + "callId": "10", + "itemId": "42", + "buyerPhone": "+79990000000", + "callTime": "2026-04-23T10:00:00Z", + "talkDuration": 60, + } + ] + }, + ) + fake.add_json( + "POST", + "/calltracking/v1/getCallById/", + { + "call": { + "callId": "10", + "itemId": "42", + "buyerPhone": "+79990000000", + "callTime": "2026-04-23T10:00:00Z", + "talkDuration": 60, + }, + "error": {"code": 0, "message": "ok"}, + }, + ) + fake.add_json( + "POST", + "/autoteka/v1/catalogs/resolve", + {"result": {"fields": [{"id": "brand", "label": "Марка", "type": "select"}]}}, + ) + fake.add_json( + "POST", + "/autoteka/v1/previews", + {"result": {"preview": {"previewId": "88", "status": "done", "vin": "XTA00000000000000"}}}, + ) + fake.add_json( + "POST", + "/autoteka/v1/reports", + { + "result": { + "report": { + "reportId": "99", + "status": "done", + "vin": "XTA00000000000000", + "createdAt": "2026-04-23T10:00:00Z", + } + } + }, + ) + fake.add_json( + "GET", + "/autoteka/v1/reports/list/", + { + "result": [ + { + "reportId": "99", + "status": "done", + "vin": "XTA00000000000000", + "createdAt": "2026-04-23T10:00:00Z", + } + ] + }, + ) + fake.add_json( + "POST", + "/core/v1/accounts/10/items/20/bookings", + {"result": "success"}, + ) + fake.add_json( + "GET", + "/realty/v1/accounts/10/items/20/bookings", + { + "bookings": [ + { + "id": 1, + "base_price": 5000, + "check_in": "2026-05-01", + "check_out": "2026-05-05", + "guest_count": 2, + "nights": 4, + "status": "active", + } + ] + }, + ) + fake.add_json( + "POST", + "/realty/v1/accounts/10/items/20/prices", + {"result": "success"}, + ) + fake.add( + "GET", + "/calltracking/v1/getRecordByCallId/", + FakeResponse( + 200, + content=b"docs-call-record", + headers={"content-type": "audio/mpeg"}, + ), + ) + fake.add_json( + "POST", + "/promotion/v1/items/services/orders/get", + { + "items": [ + { + "orderId": "ord-promo-1", + "itemId": 42, + "serviceCode": "xl", + "status": "active", + "createdAt": "2026-04-23T10:00:00Z", + } + ] + }, + ) + fake.add_json( + "POST", + "/promotion/v1/items/services/bbip/forecasts/get", + {"items": [{"itemId": 42, "min": 100, "max": 250, "totalPrice": 500}]}, + ) + fake.add_json( + "POST", + "/autostrategy/v1/budget", + { + "calcId": 55, + "budget": { + "recommended": {"total": 1000, "real": 800, "bonus": 200}, + "minimal": {"total": 500, "real": 500, "bonus": 0}, + "maximal": {"total": 2000, "real": 1800, "bonus": 200}, + "priceRanges": [], + }, + }, + ) + fake.add_json( + "POST", + "/autostrategy/v1/campaign/info", + { + "campaign": { + "campaignId": 15, + "campaignType": "AS", + "budget": 1000, + "balance": 900, + "title": "Весенняя кампания", + "statusId": 1, + "version": 3, + "userId": 7, + }, + "forecast": {"calls": {"from": 5, "to": 10}, "views": {"from": 100, "to": 250}}, + "items": [{"itemId": 42, "isActive": True}], + }, + ) + fake.add_json( + "POST", + "/autostrategy/v1/campaigns", + { + "campaigns": [ + { + "campaignId": 15, + "campaignType": "AS", + "budget": 1000, + "balance": 900, + "title": "Весенняя кампания", + "statusId": 1, + "version": 3, + "userId": 7, + } + ], + "totalCount": 1, + }, + ) + fake.add_json( + "GET", + "/autoload/v2/profile", + {"user_id": 123, "is_enabled": True, "upload_url": "https://autoload.example/upload"}, + ) + fake.add_json( + "GET", + "/autoload/v3/reports/777", + { + "report_id": 777, + "status": "done", + "created_at": "2026-04-23T10:00:00Z", + "finished_at": "2026-04-23T10:05:00Z", + "errors_count": 0, + "warnings_count": 0, + }, + ) + fake.add_json( + "GET", + "/autoload/v2/reports", + { + "reports": [ + { + "report_id": 777, + "status": "done", + "created_at": "2026-04-23T10:00:00Z", + "finished_at": "2026-04-23T10:05:00Z", + "processed_items": 1, + } + ], + "total": 1, + }, + ) fake.add_json( "GET", "/messenger/v2/accounts/7/chats", @@ -157,6 +394,22 @@ def build_docs_client() -> AvitoClient: "total": 1, }, ) + fake.add_json( + "GET", + "/messenger/v2/accounts/123/chats", + { + "chats": [ + { + "id": "chat-1", + "user_id": 123, + "title": "Покупатель", + "unread_count": 1, + "last_message": {"text": "Здравствуйте"}, + } + ], + "total": 1, + }, + ) fake.add_json( "GET", "/messenger/v2/accounts/7/chats/chat-1", @@ -191,16 +444,44 @@ def build_docs_client() -> AvitoClient: "/messenger/v1/accounts/7/chats/chat-1/messages", {"success": True, "message_id": "msg-1", "status": "sent"}, ) + fake.add_json( + "POST", + "/messenger/v1/accounts/123/chats/chat-1/messages", + {"success": True, "message_id": "msg-1", "status": "sent"}, + ) fake.add_json( "POST", "/messenger/v1/accounts/7/uploadImages", {"images": [{"image_id": "img-1", "url": "https://cdn.example/img-1.jpg"}]}, ) + fake.add_json( + "POST", + "/messenger/v1/accounts/123/uploadImages", + {"images": [{"image_id": "img-1", "url": "https://cdn.example/img-1.jpg"}]}, + ) fake.add_json( "POST", "/messenger/v1/accounts/7/chats/chat-1/messages/image", {"success": True, "message_id": "msg-img-1", "status": "sent"}, ) + fake.add_json( + "POST", + "/messenger/v1/accounts/123/chats/chat-1/messages/image", + {"success": True, "message_id": "msg-img-1", "status": "sent"}, + ) + fake.add_json( + "POST", + "/messenger/v1/subscriptions", + { + "subscriptions": [ + { + "url": "https://example.com/messenger", + "version": "v3", + "status": "active", + } + ] + }, + ) fake.add_json( "POST", "/messenger/v1/accounts/7/chats/chat-1/read", @@ -376,6 +657,11 @@ def build_docs_client() -> AvitoClient: "/job/v1/applications/webhook", {"url": "https://example.com/job", "is_active": True, "version": "v1"}, ) + fake.add_json( + "GET", + "/job/v1/applications/webhooks", + {"items": [{"url": "https://example.com/job", "is_active": True, "version": "v1"}]}, + ) fake.add_json( "PUT", "/job/v1/applications/webhook", diff --git a/tests/docs/test_markdown_examples.py b/tests/docs/test_markdown_examples.py index 72ce1f9..a839997 100644 --- a/tests/docs/test_markdown_examples.py +++ b/tests/docs/test_markdown_examples.py @@ -1,28 +1,37 @@ from __future__ import annotations -import re from pathlib import Path +import mktestdocs + DOCS_ROOT = Path(__file__).resolve().parents[2] EXECUTABLE_MARKDOWN = [ + DOCS_ROOT / "README.md", *sorted((DOCS_ROOT / "docs/site/tutorials").glob("*.md")), *sorted((DOCS_ROOT / "docs/site/how-to").glob("*.md")), ] -PYTHON_BLOCK = re.compile(r"```(?:python|pycon)\n(.*?)\n```", re.DOTALL) -def executable_blocks(path: Path) -> list[str]: - return [match.group(1) for match in PYTHON_BLOCK.finditer(path.read_text(encoding="utf-8"))] +def execute_pycon(source: str) -> None: + lines: list[str] = [] + for line in source.splitlines(): + if line.startswith(">>> "): + lines.append(line[4:]) + elif line.startswith("... "): + lines.append(line[4:]) + exec("\n".join(lines), {"__name__": "__main__"}) + +mktestdocs.register_executor("pycon", execute_pycon) -def test_tutorial_and_howto_python_examples_execute_without_network() -> None: - namespace: dict[str, object] = {} - blocks = [(path, block) for path in EXECUTABLE_MARKDOWN for block in executable_blocks(path)] - assert blocks, "В tutorials/how-to должен быть хотя бы один исполняемый Python-пример." +def test_readme_tutorial_and_howto_python_examples_execute_without_network() -> None: + assert EXECUTABLE_MARKDOWN, "README/tutorials/how-to должны проверяться mktestdocs." - for path, block in blocks: + for path in EXECUTABLE_MARKDOWN: try: - exec(compile(block, str(path), "exec"), namespace) + mktestdocs.check_md_file(path, lang="python") + mktestdocs.check_md_file(path, lang="pycon") except Exception as exc: # noqa: BLE001 - raise AssertionError(f"Python-пример из {path} не выполнился.") from exc + relative_path = path.relative_to(DOCS_ROOT) + raise AssertionError(f"Python/pycon-пример из {relative_path} не выполнился.") from exc From 7bc23171bfb672a9eabe0c07e56affce8b420bd6 Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Fri, 24 Apr 2026 01:57:13 +0300 Subject: [PATCH 7/9] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pull_request_template.md | 7 + .github/workflows/ci.yml | 7 +- .github/workflows/docs.yml | 21 +- .gitignore | 3 + CHANGELOG.md | 9 + CONTRIBUTING.md | 58 ++++ Makefile | 2 + avito/accounts/domain.py | 50 +++- avito/ads/domain.py | 176 +++++++++--- avito/autoteka/domain.py | 206 +++++++++++++- avito/client.py | 5 +- avito/cpa/domain.py | 86 +++++- avito/jobs/domain.py | 176 +++++++++++- avito/messenger/domain.py | 116 ++++++-- avito/orders/domain.py | 388 ++++++++++++++++++++++++++- avito/promotion/domain.py | 175 +++++++++--- avito/ratings/domain.py | 32 +++ avito/realty/domain.py | 49 ++++ avito/tariffs/domain.py | 5 +- docs/site/assets/_gen_reference.py | 4 +- docs/site/reference/testing.md | 2 + poetry.lock | 94 ++++++- pyproject.toml | 1 + scripts/build_docs_quality_report.py | 205 ++++++++++++-- scripts/check_changelog_sections.py | 64 +++++ scripts/check_inventory_coverage.py | 24 +- scripts/check_public_docstrings.py | 121 ++++++--- scripts/public_sdk_surface.py | 241 +++++++++++++++++ 28 files changed, 2133 insertions(+), 194 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md create mode 100644 scripts/check_changelog_sections.py create mode 100644 scripts/public_sdk_surface.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..2c443e2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +## Проверки + +- [ ] `make check` проходит локально или в CI. +- [ ] `make docs-strict` проходит, если изменены README, docs, публичные сигнатуры или inventory. +- [ ] README/tutorials/how-to примеры соответствуют актуальным публичным сигнатурам SDK. +- [ ] Новая публичная операция добавлена в `docs/avito/inventory.md` и покрыта reference. +- [ ] Публичное переименование: alias сохранён, `DeprecationWarning` добавлен, `CHANGELOG.md` обновлён в секции `Deprecated`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11f5286..2344e04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python 3.14 uses: actions/setup-python@v5 @@ -27,11 +29,14 @@ jobs: virtualenvs-in-project: true - name: Install dependencies - run: poetry install --no-interaction + run: poetry install --no-interaction --with docs - name: Run quality gate run: make check + - name: Run docs strict gate + run: make docs-strict + - name: Collect coverage report run: | poetry run coverage run -m pytest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 254cc72..bd283ee 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,11 +43,15 @@ jobs: poetry run python scripts/check_spec_inventory_sync.py --output spec-inventory-report.json poetry run python scripts/check_reference_public_surface.py --output reference-public-report.json poetry run python scripts/check_public_docstrings.py --output docstring-contract-report.json + poetry run python scripts/check_changelog_sections.py --output changelog-sections-report.json + poetry run bandit -r avito -lll -f json -o bandit-report.json poetry run python scripts/build_docs_quality_report.py \ --inventory-report inventory-coverage-report.json \ --spec-report spec-inventory-report.json \ --reference-report reference-public-report.json \ --docstring-report docstring-contract-report.json \ + --changelog-report changelog-sections-report.json \ + --bandit-report bandit-report.json \ --output docs-quality-report.json - name: Upload docs reports @@ -59,8 +63,15 @@ jobs: spec-inventory-report.json reference-public-report.json docstring-contract-report.json + changelog-sections-report.json + bandit-report.json docs-quality-report.json + - name: Check links + uses: lycheeverse/lychee-action@v2 + with: + args: --exclude "avito\.ru" --retry-wait-time 5 --max-retries 3 --timeout 30 site/ + deploy: runs-on: ubuntu-latest needs: build @@ -91,4 +102,12 @@ jobs: - name: Deploy latest (push to main) if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' - run: poetry run mike deploy --push --update-aliases main latest + run: | + poetry run mike deploy --push --update-aliases main latest + poetry run mike set-default --push latest + + - name: Deploy stable (push tag) + if: startsWith(github.ref, 'refs/tags/v') + run: | + VERSION="${GITHUB_REF_NAME#v}" + poetry run mike deploy --push --update-aliases "$VERSION" stable diff --git a/.gitignore b/.gitignore index 66ed71a..807e6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -131,7 +131,10 @@ inventory-coverage-report.json spec-inventory-report.json reference-public-report.json docstring-contract-report.json +changelog-sections-report.json +bandit-report.json docs-quality-report.json +ttfc-minutes.txt # mypy .mypy_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb98a7..2f6b8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to Semantic Versioning. ## [Unreleased] +### Added +- Нет изменений. + ### Deprecated - Архивные CPA-методы `CpaArchive.get_call`, `CpaArchive.get_balance_info`, `CpaArchive.get_call_by_id` и режим `CpaChat.list(version=1)` теперь эмитируют `DeprecationWarning` при первом вызове; используйте `call_tracking_call().download`, `cpa_lead().get_balance_info`, `call_tracking_call().get` и `cpa_chat().list(version=2)`. - Архивные методы автозагрузки `AutoloadArchive.get_profile`, `AutoloadArchive.save_profile`, `AutoloadArchive.get_last_completed_report`, `AutoloadArchive.get_report` теперь эмитируют `DeprecationWarning` при первом вызове; используйте `autoload_profile().get`, `autoload_profile().save`, `autoload_report().get_last_completed` и `autoload_report().get`. @@ -20,6 +23,12 @@ and this project adheres to Semantic Versioning. - Transport получил поддержку `Idempotency-Key`; публичные write-методы во всех доменах принимают `idempotency_key`, а dry-run/write-контракт promotion покрыт тестами. - Во всех доменных пакетах добавлены `enums.py`; `accounts`, `ads`, `autoteka`, `jobs`, `messenger`, `orders`, `promotion`, `ratings`, `realty` и `tariffs` переведены на typed enums с fallback на `UNKNOWN` и warning-логом ровно один раз на неизвестное upstream-значение. +### Removed +- Нет изменений. + +### Fixed +- Нет изменений. + ## [1.0.2] - 2026-04-21 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3ae6779 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing + +## Проверки документации + +Перед PR, который меняет публичный API, README или `docs/site/`, выполните: + +```bash +make docs-strict +make docs-report +``` + +Для полной локальной проверки ссылок установите `lychee`: + +```bash +brew install lychee +``` + +или на Linux: + +```bash +cargo binstall lychee +``` + +После установки доступна проверка: + +```bash +make docs-check +``` + +## TTFC measurement + +TTFC показывает, за сколько минут новый пользователь проходит путь от чистого +окружения до первого реального `get_self()` из `docs/site/tutorials/getting-started.md`. +Цель документации: не больше 15 минут. + +Процедура перед релизом: + +1. Создайте новый временный каталог и виртуальное окружение. +2. Установите опубликованный пакет: `pip install avito-py`. +3. Установите реальные `AVITO_CLIENT_ID` и `AVITO_CLIENT_SECRET`. +4. Запустите секундомер. +5. Выполните tutorial `getting-started.md` до успешного `get_self()`. +6. Остановите секундомер и запишите результат в минутах. +7. Перед сборкой отчёта передайте значение одним из способов: + +```bash +TTFC_MINUTES=8.5 make docs-report +``` + +или: + +```bash +printf "8.5\n" > ttfc-minutes.txt +make docs-report +``` + +`ttfc-minutes.txt` не коммитится. В CI релизного прогона можно передать +`--ttfc-minutes ` в `scripts/build_docs_quality_report.py`. diff --git a/Makefile b/Makefile index 7c40fe2..1656abd 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,8 @@ docs-report: poetry run python scripts/check_spec_inventory_sync.py --output spec-inventory-report.json poetry run python scripts/check_reference_public_surface.py --output reference-public-report.json poetry run python scripts/check_public_docstrings.py --output docstring-contract-report.json + poetry run python scripts/check_changelog_sections.py --output changelog-sections-report.json + poetry run bandit -r avito -lll -f json -o bandit-report.json poetry run python scripts/build_docs_quality_report.py docs-check: docs-strict diff --git a/avito/accounts/domain.py b/avito/accounts/domain.py index 16d83b1..e21c8e3 100644 --- a/avito/accounts/domain.py +++ b/avito/accounts/domain.py @@ -32,12 +32,18 @@ class Account(DomainObject): user_id: int | str | None = None def get_self(self) -> AccountProfile: - """Получает профиль авторизованного пользователя.""" + """Получает профиль авторизованного пользователя. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AccountsClient(self.transport).get_self() def get_balance(self, user_id: int | None = None) -> AccountBalance: - """Получает баланс пользователя.""" + """Получает баланс пользователя. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_user_id = user_id or (int(self.user_id) if self.user_id is not None else None) if resolved_user_id is None: @@ -52,7 +58,12 @@ def get_operations_history( limit: int | None = None, offset: int | None = None, ) -> PaginatedList[OperationRecord]: - """Получает историю операций пользователя.""" + """Получает историю операций пользователя. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AccountsClient(self.transport).get_operations_history( date_from=_serialize_datetime(date_from), @@ -69,17 +80,30 @@ class AccountHierarchy(DomainObject): user_id: int | str | None = None def get_status(self) -> AhUserStatus: - """Получает статус пользователя в ИА.""" + """Получает статус пользователя в ИА. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return HierarchyClient(self.transport).get_status() def list_employees(self) -> EmployeesResult: - """Получает список сотрудников иерархии.""" + """Получает список сотрудников иерархии. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return HierarchyClient(self.transport).list_employees() def list_company_phones(self) -> CompanyPhonesResult: - """Получает список телефонов компании.""" + """Получает список телефонов компании. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return HierarchyClient(self.transport).list_company_phones() @@ -91,7 +115,12 @@ def link_items( source_employee_id: int | None = None, idempotency_key: str | None = None, ) -> AccountActionResult: - """Прикрепляет объявления к сотруднику.""" + """Прикрепляет объявления к сотруднику. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return HierarchyClient(self.transport).link_items( employee_id=employee_id, @@ -107,7 +136,12 @@ def list_items_by_employee( limit: int | None = None, offset: int | None = None, ) -> PaginatedList[EmployeeItem]: - """Получает список объявлений сотрудника.""" + """Получает список объявлений сотрудника. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return HierarchyClient(self.transport).list_items_by_employee( employee_id=employee_id, diff --git a/avito/ads/domain.py b/avito/ads/domain.py index 188e8fd..d2062d8 100644 --- a/avito/ads/domain.py +++ b/avito/ads/domain.py @@ -75,7 +75,10 @@ class Ad(DomainObject): user_id: int | str | None = None def get(self) -> Listing: - """Получает объявление по `item_id`.""" + """Получает объявление по `item_id`. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ item_id, user_id = self._require_ids() return AdsClient(self.transport).get_item(user_id=user_id, item_id=item_id) @@ -87,7 +90,12 @@ def list( limit: int | None = None, offset: int | None = None, ) -> PaginatedList[Listing]: - """Получает список объявлений.""" + """Получает список объявлений. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ user_id = int(self.user_id) if self.user_id is not None else None return AdsClient(self.transport).list_items( @@ -100,7 +108,12 @@ def update_price( price: int | float, idempotency_key: str | None = None, ) -> UpdatePriceResult: - """Обновляет цену текущего объявления.""" + """Обновляет цену текущего объявления. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ item_id = self._require_item_id() return AdsClient(self.transport).update_price( @@ -134,12 +147,13 @@ def get_calls_stats( date_from: datetime | None = None, date_to: datetime | None = None, ) -> CallsStatsResult: - """Получает статистику звонков.""" + """Получает статистику звонков. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ user_id = self._require_user_id() - resolved_item_ids = item_ids or ( - [int(self.item_id)] if self.item_id is not None else [] - ) + resolved_item_ids = item_ids or ([int(self.item_id)] if self.item_id is not None else []) return StatsClient(self.transport).get_calls_stats( user_id=user_id, item_ids=resolved_item_ids, @@ -155,12 +169,13 @@ def get_item_stats( date_to: datetime | None = None, fields: list[str] | None = None, ) -> ItemStatsResult: - """Получает статистику по списку объявлений.""" + """Получает статистику по списку объявлений. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ user_id = self._require_user_id() - resolved_item_ids = item_ids or ( - [int(self.item_id)] if self.item_id is not None else [] - ) + resolved_item_ids = item_ids or ([int(self.item_id)] if self.item_id is not None else []) return StatsClient(self.transport).get_item_stats( user_id=user_id, item_ids=resolved_item_ids, @@ -177,12 +192,13 @@ def get_item_analytics( date_to: datetime | None = None, fields: list[str] | None = None, ) -> ItemAnalyticsResult: - """Получает аналитику по профилю.""" + """Получает аналитику по профилю. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ user_id = self._require_user_id() - resolved_item_ids = item_ids or ( - [int(self.item_id)] if self.item_id is not None else [] - ) + resolved_item_ids = item_ids or ([int(self.item_id)] if self.item_id is not None else []) return StatsClient(self.transport).get_item_analytics( user_id=user_id, item_ids=resolved_item_ids, @@ -199,12 +215,13 @@ def get_account_spendings( date_to: datetime | None = None, fields: list[str] | None = None, ) -> AccountSpendings: - """Получает статистику расходов профиля.""" + """Получает статистику расходов профиля. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ user_id = self._require_user_id() - resolved_item_ids = item_ids or ( - [int(self.item_id)] if self.item_id is not None else [] - ) + resolved_item_ids = item_ids or ([int(self.item_id)] if self.item_id is not None else []) return StatsClient(self.transport).get_account_spendings( user_id=user_id, item_ids=resolved_item_ids, @@ -229,7 +246,10 @@ class AdPromotion(DomainObject): def get_vas_prices( self, *, item_ids: list[int], location_id: int | None = None ) -> VasPricesResult: - """Получает цены продвижения и доступные услуги.""" + """Получает цены продвижения и доступные услуги. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ user_id = self._require_user_id() return VasClient(self.transport).get_prices( @@ -245,7 +265,14 @@ def apply_vas( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Применяет дополнительные услуги к объявлению.""" + """Применяет дополнительные услуги к объявлению. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ item_id, user_id = self._require_ids() validate_string_items("codes", codes) @@ -271,7 +298,14 @@ def apply_vas_package( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Применяет пакет дополнительных услуг.""" + """Применяет пакет дополнительных услуг. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ item_id, user_id = self._require_ids() validate_non_empty_string("package_code", package_code) @@ -297,7 +331,12 @@ def apply_vas_direct( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Применяет услуги продвижения через прямой v2 endpoint.""" + """Применяет услуги продвижения через прямой v2 endpoint. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + """ item_id = self._require_item_id() validate_string_items("codes", codes) @@ -336,7 +375,10 @@ class AutoloadProfile(DomainObject): user_id: int | str | None = None def get(self) -> AutoloadProfileSettings: - """Получает профиль автозагрузки.""" + """Получает профиль автозагрузки. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).get_profile() @@ -348,7 +390,12 @@ def save( callback_url: str | None = None, idempotency_key: str | None = None, ) -> AdsActionResult: - """Сохраняет профиль автозагрузки.""" + """Сохраняет профиль автозагрузки. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).save_profile( is_enabled=is_enabled, @@ -358,7 +405,12 @@ def save( ) def upload_by_url(self, *, url: str, idempotency_key: str | None = None) -> UploadResult: - """Загружает файл по ссылке.""" + """Загружает файл по ссылке. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).upload_by_url( url=url, @@ -366,12 +418,18 @@ def upload_by_url(self, *, url: str, idempotency_key: str | None = None) -> Uplo ) def get_tree(self) -> AutoloadTreeResult: - """Получает дерево категорий.""" + """Получает дерево категорий. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).get_tree() def get_node_fields(self, *, node_slug: str) -> AutoloadFieldsResult: - """Получает поля категории.""" + """Получает поля категории. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).get_node_fields(node_slug=node_slug) @@ -383,7 +441,10 @@ class AutoloadReport(DomainObject): report_id: int | str | None = None def get(self) -> AutoloadReportDetails: - """Получает конкретный отчет v3.""" + """Получает конкретный отчет v3. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ report_id = self._require_report_id() return AutoloadClient(self.transport).get_report(report_id=report_id) @@ -391,39 +452,64 @@ def get(self) -> AutoloadReportDetails: def list( self, *, limit: int | None = None, offset: int | None = None ) -> PaginatedList[AutoloadReportSummary]: - """Получает список отчетов автозагрузки.""" + """Получает список отчетов автозагрузки. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).list_reports(limit=limit, offset=offset) def get_last_completed(self) -> AutoloadReportDetails: - """Получает последний завершенный отчет.""" + """Получает последний завершенный отчет. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).get_last_completed_report() def get_items(self) -> AutoloadReportItemsResult: - """Получает объявления из отчета.""" + """Получает объявления из отчета. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ report_id = self._require_report_id() return AutoloadClient(self.transport).get_report_items(report_id=report_id) def get_fees(self) -> AutoloadFeesResult: - """Получает списания по объявлениям отчета.""" + """Получает списания по объявлениям отчета. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ report_id = self._require_report_id() return AutoloadClient(self.transport).get_report_fees(report_id=report_id) def get_ad_ids_by_avito_ids(self, *, avito_ids: Sequence[int]) -> IdMappingResult: - """Получает ad ids по avito ids.""" + """Получает ad ids по avito ids. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).get_ad_ids_by_avito_ids(avito_ids=list(avito_ids)) def get_avito_ids_by_ad_ids(self, *, ad_ids: Sequence[int]) -> IdMappingResult: - """Получает avito ids по ad ids.""" + """Получает avito ids по ad ids. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).get_avito_ids_by_ad_ids(ad_ids=list(ad_ids)) def get_items_info(self, *, item_ids: Sequence[int]) -> AutoloadReportItemsResult: - """Получает информацию по объявлениям автозагрузки.""" + """Получает информацию по объявлениям автозагрузки. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutoloadClient(self.transport).get_items_info(item_ids=list(item_ids)) @@ -448,7 +534,9 @@ class AutoloadArchive(DomainObject): def get_profile(self) -> AutoloadProfileSettings: """Получает архивный профиль автозагрузки. - Deprecated: используйте `autoload_profile().get`; удаление в версии 1.3.0. + Deprecated: используйте `autoload_profile().get`; удаление в версии 1.3.0. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. """ return AutoloadArchiveClient(self.transport).get_profile() @@ -469,7 +557,11 @@ def save_profile( ) -> AdsActionResult: """Сохраняет архивный профиль автозагрузки. - Deprecated: используйте `autoload_profile().save`; удаление в версии 1.3.0. + Deprecated: используйте `autoload_profile().save`; удаление в версии 1.3.0. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. """ return AutoloadArchiveClient(self.transport).save_profile( @@ -488,7 +580,9 @@ def save_profile( def get_last_completed_report(self) -> LegacyAutoloadReport: """Получает архивную статистику по последней выгрузке. - Deprecated: используйте `autoload_report().get_last_completed`; удаление в версии 1.3.0. + Deprecated: используйте `autoload_report().get_last_completed`; удаление в версии 1.3.0. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. """ return AutoloadArchiveClient(self.transport).get_last_completed_report() @@ -502,7 +596,9 @@ def get_last_completed_report(self) -> LegacyAutoloadReport: def get_report(self) -> LegacyAutoloadReport: """Получает архивную статистику по конкретной выгрузке. - Deprecated: используйте `autoload_report().get`; удаление в версии 1.3.0. + Deprecated: используйте `autoload_report().get`; удаление в версии 1.3.0. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. """ report_id = self._require_report_id() diff --git a/avito/autoteka/domain.py b/avito/autoteka/domain.py index 0d87868..ac38247 100644 --- a/avito/autoteka/domain.py +++ b/avito/autoteka/domain.py @@ -43,22 +43,48 @@ class AutotekaVehicle(DomainObject): user_id: int | str | None = None def resolve_catalog(self, *, brand_id: int) -> CatalogResolveResult: - """Актуализирует параметры автокаталога.""" + """Актуализирует параметры автокаталога. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return CatalogClient(self.transport).resolve_catalog(brand_id=brand_id) def get_leads(self, *, limit: int) -> AutotekaLeadsResult: + """Выполняет публичную операцию `AutotekaVehicle.get_leads` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return LeadsClient(self.transport).get_leads(limit=limit) def create_preview_by_vin( self, *, vin: str, idempotency_key: str | None = None ) -> AutotekaPreviewInfo: + """Выполняет публичную операцию `AutotekaVehicle.create_preview_by_vin` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return PreviewClient(self.transport).create_by_vin( vin=vin, idempotency_key=idempotency_key, ) def get_preview(self, *, preview_id: int | str | None = None) -> AutotekaPreviewInfo: + """Выполняет публичную операцию `AutotekaVehicle.get_preview` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return PreviewClient(self.transport).get_preview( preview_id=preview_id or self._require_vehicle_id("preview_id") ) @@ -70,6 +96,15 @@ def create_preview_by_external_item( site: str, idempotency_key: str | None = None, ) -> AutotekaPreviewInfo: + """Выполняет публичную операцию `AutotekaVehicle.create_preview_by_external_item` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return PreviewClient(self.transport).create_by_external_item( item_id=item_id, site=site, @@ -79,6 +114,15 @@ def create_preview_by_external_item( def create_preview_by_item_id( self, *, item_id: int, idempotency_key: str | None = None ) -> AutotekaPreviewInfo: + """Выполняет публичную операцию `AutotekaVehicle.create_preview_by_item_id` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return PreviewClient(self.transport).create_by_item_id( item_id=item_id, idempotency_key=idempotency_key, @@ -87,6 +131,15 @@ def create_preview_by_item_id( def create_preview_by_reg_number( self, *, reg_number: str, idempotency_key: str | None = None ) -> AutotekaPreviewInfo: + """Выполняет публичную операцию `AutotekaVehicle.create_preview_by_reg_number` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return PreviewClient(self.transport).create_by_reg_number( reg_number=reg_number, idempotency_key=idempotency_key, @@ -95,6 +148,15 @@ def create_preview_by_reg_number( def create_specification_by_plate_number( self, *, plate_number: str, idempotency_key: str | None = None ) -> AutotekaSpecificationInfo: + """Выполняет публичную операцию `AutotekaVehicle.create_specification_by_plate_number` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SpecificationsClient(self.transport).create_by_plate_number( plate_number=plate_number, idempotency_key=idempotency_key, @@ -103,6 +165,15 @@ def create_specification_by_plate_number( def create_specification_by_vehicle_id( self, *, vehicle_id: str, idempotency_key: str | None = None ) -> AutotekaSpecificationInfo: + """Выполняет публичную операцию `AutotekaVehicle.create_specification_by_vehicle_id` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SpecificationsClient(self.transport).create_by_vehicle_id( vehicle_id=vehicle_id, idempotency_key=idempotency_key, @@ -113,6 +184,13 @@ def get_specification_by_id( *, specification_id: int | str | None = None, ) -> AutotekaSpecificationInfo: + """Выполняет публичную операцию `AutotekaVehicle.get_specification_by_id` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SpecificationsClient(self.transport).get_by_id( specification_id=specification_id or self._require_vehicle_id("specification_id") ) @@ -120,12 +198,28 @@ def get_specification_by_id( def create_teaser( self, *, vehicle_id: str, idempotency_key: str | None = None ) -> AutotekaTeaserInfo: + """Выполняет публичную операцию `AutotekaVehicle.create_teaser` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return TeaserClient(self.transport).create( vehicle_id=vehicle_id, idempotency_key=idempotency_key, ) def get_teaser(self, *, teaser_id: int | str | None = None) -> AutotekaTeaserInfo: + """Выполняет публичную операцию `AutotekaVehicle.get_teaser` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return TeaserClient(self.transport).get( teaser_id=teaser_id or self._require_vehicle_id("teaser_id") ) @@ -144,11 +238,27 @@ class AutotekaReport(DomainObject): user_id: int | str | None = None def get_active_package(self) -> AutotekaPackageInfo: + """Выполняет публичную операцию `AutotekaReport.get_active_package` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ReportClient(self.transport).get_active_package() def create_report( self, *, preview_id: int, idempotency_key: str | None = None ) -> AutotekaReportInfo: + """Выполняет публичную операцию `AutotekaReport.create_report` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ReportClient(self.transport).create_report( preview_id=preview_id, idempotency_key=idempotency_key, @@ -157,17 +267,38 @@ def create_report( def create_report_by_vehicle_id( self, *, vehicle_id: str, idempotency_key: str | None = None ) -> AutotekaReportInfo: + """Выполняет публичную операцию `AutotekaReport.create_report_by_vehicle_id` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ReportClient(self.transport).create_report_by_vehicle_id( vehicle_id=vehicle_id, idempotency_key=idempotency_key, ) def list_reports(self) -> AutotekaReportsResult: - """Получает список отчетов Автотеки.""" + """Получает список отчетов Автотеки. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return ReportClient(self.transport).list_reports() def get_report(self, *, report_id: int | str | None = None) -> AutotekaReportInfo: + """Выполняет публичную операцию `AutotekaReport.get_report` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ReportClient(self.transport).get_report( report_id=report_id or self._require_report_id() ) @@ -175,6 +306,15 @@ def get_report(self, *, report_id: int | str | None = None) -> AutotekaReportInf def create_sync_report_by_reg_number( self, *, reg_number: str, idempotency_key: str | None = None ) -> AutotekaReportInfo: + """Выполняет публичную операцию `AutotekaReport.create_sync_report_by_reg_number` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ReportClient(self.transport).create_sync_report_by_reg_number( reg_number=reg_number, idempotency_key=idempotency_key, @@ -183,6 +323,15 @@ def create_sync_report_by_reg_number( def create_sync_report_by_vin( self, *, vin: str, idempotency_key: str | None = None ) -> AutotekaReportInfo: + """Выполняет публичную операцию `AutotekaReport.create_sync_report_by_vin` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ReportClient(self.transport).create_sync_report_by_vin( vin=vin, idempotency_key=idempotency_key, @@ -203,20 +352,39 @@ class AutotekaMonitoring(DomainObject): def create_monitoring_bucket_add( self, *, vehicles: list[str], idempotency_key: str | None = None ) -> MonitoringBucketResult: + """Выполняет публичную операцию `AutotekaMonitoring.create_monitoring_bucket_add` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return MonitoringClient(self.transport).add_bucket( vehicles=vehicles, idempotency_key=idempotency_key, ) def delete_bucket(self, *, idempotency_key: str | None = None) -> MonitoringBucketResult: - """Очищает bucket мониторинга.""" + """Очищает bucket мониторинга. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MonitoringClient(self.transport).delete_bucket(idempotency_key=idempotency_key) def remove_bucket( self, *, vehicles: list[str], idempotency_key: str | None = None ) -> MonitoringBucketResult: - """Удаляет автомобили из bucket мониторинга.""" + """Удаляет автомобили из bucket мониторинга. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MonitoringClient(self.transport).remove_bucket( vehicles=vehicles, @@ -228,6 +396,13 @@ def get_monitoring_reg_actions( *, query: MonitoringEventsQuery | None = None, ) -> MonitoringEventsResult: + """Выполняет публичную операцию `AutotekaMonitoring.get_monitoring_reg_actions` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return MonitoringClient(self.transport).get_reg_actions(query=query) @@ -241,12 +416,28 @@ class AutotekaScoring(DomainObject): def create_scoring_by_vehicle_id( self, *, vehicle_id: str, idempotency_key: str | None = None ) -> AutotekaScoringInfo: + """Выполняет публичную операцию `AutotekaScoring.create_scoring_by_vehicle_id` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ScoringClient(self.transport).create_by_vehicle_id( vehicle_id=vehicle_id, idempotency_key=idempotency_key, ) def get_scoring_by_id(self, *, scoring_id: int | str | None = None) -> AutotekaScoringInfo: + """Выполняет публичную операцию `AutotekaScoring.get_scoring_by_id` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ScoringClient(self.transport).get_by_id( scoring_id=scoring_id or self._require_scoring_id() ) @@ -266,6 +457,13 @@ class AutotekaValuation(DomainObject): def get_valuation_by_specification( self, *, specification_id: int, mileage: int ) -> AutotekaValuationInfo: + """Выполняет публичную операцию `AutotekaValuation.get_valuation_by_specification` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ValuationClient(self.transport).get_by_specification( ValuationBySpecificationRequest(specification_id=specification_id, mileage=mileage) ) diff --git a/avito/client.py b/avito/client.py index e21b151..552e261 100644 --- a/avito/client.py +++ b/avito/client.py @@ -90,7 +90,10 @@ def _from_transport( return client def auth(self) -> AuthProvider: - """Возвращает объект аутентификации и token-flow операций.""" + """Возвращает объект аутентификации и token-flow операций. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ self._ensure_open() return self.auth_provider diff --git a/avito/cpa/domain.py b/avito/cpa/domain.py index 5c69714..9b8dfdf 100644 --- a/avito/cpa/domain.py +++ b/avito/cpa/domain.py @@ -42,12 +42,26 @@ def create_complaint_by_action_id( action_id: str, reason: str, ) -> CpaActionResult: + """Выполняет публичную операцию `CpaLead.create_complaint_by_action_id` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CpaLeadsClient(self.transport).create_complaint_by_action_id( action_id=action_id, reason=reason, ) def get_balance_info(self) -> CpaBalanceInfo: + """Выполняет публичную операцию `CpaLead.get_balance_info` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CpaLeadsClient(self.transport).get_balance_info() @@ -59,6 +73,13 @@ class CpaChat(DomainObject): user_id: int | str | None = None def get(self, *, action_id: int | str | None = None) -> CpaChatInfo: + """Выполняет публичную операцию `CpaChat.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CpaChatsClient(self.transport).get_by_action_id( action_id=action_id or self._require_action_id() ) @@ -70,6 +91,13 @@ def list( limit: int | None = None, version: int = 2, ) -> CpaChatsResult: + """Выполняет публичную операцию `CpaChat.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + client = CpaChatsClient(self.transport) if version == 1: warn_deprecated_once( @@ -86,6 +114,13 @@ def get_phones_info_from_chats( *, action_ids: Sequence[str], ) -> CpaPhonesResult: + """Выполняет публичную операцию `CpaChat.get_phones_info_from_chats` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CpaChatsClient(self.transport).get_phones_info(action_ids=list(action_ids)) def _require_action_id(self) -> str: @@ -101,12 +136,26 @@ class CpaCall(DomainObject): user_id: int | str | None = None def list(self, *, date_time_from: str, date_time_to: str) -> CpaCallsResult: + """Выполняет публичную операцию `CpaCall.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CpaCallsClient(self.transport).list_by_time( date_time_from=date_time_from, date_time_to=date_time_to, ) def create_complaint(self, *, call_id: int, reason: str) -> CpaActionResult: + """Выполняет публичную операцию `CpaCall.create_complaint` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CpaCallsClient(self.transport).create_complaint(call_id=call_id, reason=reason) @@ -126,7 +175,9 @@ class CpaArchive(DomainObject): def get_call(self, *, call_id: int | str | None = None) -> CpaAudioRecord: """Получает архивную запись звонка. - Deprecated: используйте `call_tracking_call().download`; удаление в версии 1.3.0. + Deprecated: используйте `call_tracking_call().download`; удаление в версии 1.3.0. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. """ return CpaArchiveClient(self.transport).get_record( @@ -142,7 +193,9 @@ def get_call(self, *, call_id: int | str | None = None) -> CpaAudioRecord: def get_balance_info(self) -> CpaBalanceInfo: """Получает архивный баланс CPA. - Deprecated: используйте `cpa_lead().get_balance_info`; удаление в версии 1.3.0. + Deprecated: используйте `cpa_lead().get_balance_info`; удаление в версии 1.3.0. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. """ return CpaArchiveClient(self.transport).get_balance_info() @@ -156,7 +209,9 @@ def get_balance_info(self) -> CpaBalanceInfo: def get_call_by_id(self, *, call_id: int) -> CpaCallInfo: """Получает архивные данные звонка. - Deprecated: используйте `call_tracking_call().get`; удаление в версии 1.3.0. + Deprecated: используйте `call_tracking_call().get`; удаление в версии 1.3.0. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. """ return CpaArchiveClient(self.transport).get_call_by_id(call_id=call_id) @@ -175,9 +230,14 @@ class CallTrackingCall(DomainObject): user_id: int | str | None = None def get(self, *, call_id: int | None = None) -> CallTrackingCallResponse: - resolved_call_id = call_id or ( - int(self.call_id) if self.call_id is not None else None - ) + """Выполняет публичную операцию `CallTrackingCall.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + + resolved_call_id = call_id or (int(self.call_id) if self.call_id is not None else None) if resolved_call_id is None: raise ValidationError("Для операции требуется `call_id`.") return CallTrackingClient(self.transport).get_call_by_id(call_id=resolved_call_id) @@ -190,6 +250,13 @@ def list( limit: int | None = None, offset: int | None = None, ) -> CallTrackingCallsResult: + """Выполняет публичную операцию `CallTrackingCall.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CallTrackingClient(self.transport).get_calls( date_time_from=date_time_from, date_time_to=date_time_to, @@ -198,6 +265,13 @@ def list( ) def download(self, *, call_id: int | str | None = None) -> CallTrackingRecord: + """Выполняет публичную операцию `CallTrackingCall.download` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return CallTrackingClient(self.transport).get_record_by_call_id( call_id=call_id or self._require_call_id() ) diff --git a/avito/jobs/domain.py b/avito/jobs/domain.py index 5107e7e..4ab6130 100644 --- a/avito/jobs/domain.py +++ b/avito/jobs/domain.py @@ -50,6 +50,15 @@ def create( version: int = 2, idempotency_key: str | None = None, ) -> JobActionResult: + """Выполняет публичную операцию `Vacancy.create` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + client = VacanciesClient(self.transport) if version == 1: return client.create_classic(title=title, idempotency_key=idempotency_key) @@ -64,6 +73,15 @@ def update( version: int = 2, idempotency_key: str | None = None, ) -> JobActionResult: + """Выполняет публичную операцию `Vacancy.update` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + client = VacanciesClient(self.transport) if version == 1: return client.update_classic( @@ -84,6 +102,15 @@ def delete( vacancy_id: int | str | None = None, idempotency_key: str | None = None, ) -> JobActionResult: + """Выполняет публичную операцию `Vacancy.delete` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return VacanciesClient(self.transport).archive( vacancy_id=vacancy_id or self._require_vacancy_id(), employee_id=employee_id, @@ -97,6 +124,15 @@ def prolongate( vacancy_id: int | str | None = None, idempotency_key: str | None = None, ) -> JobActionResult: + """Выполняет публичную операцию `Vacancy.prolongate` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return VacanciesClient(self.transport).prolongate( vacancy_id=vacancy_id or self._require_vacancy_id(), billing_type=billing_type, @@ -104,20 +140,48 @@ def prolongate( ) def list(self, *, query: VacanciesQuery | None = None) -> VacanciesResult: + """Выполняет публичную операцию `Vacancy.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return VacanciesClient(self.transport).list(query=query) def get( self, *, vacancy_id: int | str | None = None, query: VacanciesQuery | None = None ) -> VacancyInfo: + """Выполняет публичную операцию `Vacancy.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return VacanciesClient(self.transport).get_item( vacancy_id=vacancy_id or self._require_vacancy_id(), query=query, ) def get_by_ids(self, *, ids: Sequence[int]) -> VacanciesResult: + """Выполняет публичную операцию `Vacancy.get_by_ids` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return VacanciesClient(self.transport).get_by_ids(ids=list(ids)) def get_statuses(self, *, ids: Sequence[int]) -> VacancyStatusesResult: + """Выполняет публичную операцию `Vacancy.get_statuses` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return VacanciesClient(self.transport).get_statuses(ids=list(ids)) def update_auto_renewal( @@ -127,6 +191,15 @@ def update_auto_renewal( vacancy_uuid: str | None = None, idempotency_key: str | None = None, ) -> JobActionResult: + """Выполняет публичную операцию `Vacancy.update_auto_renewal` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return VacanciesClient(self.transport).update_auto_renewal( vacancy_uuid=vacancy_uuid or self._require_vacancy_id(), auto_renewal=auto_renewal, @@ -152,6 +225,15 @@ def apply( action: str, idempotency_key: str | None = None, ) -> JobActionResult: + """Выполняет публичную операцию `Application.apply` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ApplicationsClient(self.transport).apply_actions( ids=list(ids), action=action, @@ -164,6 +246,13 @@ def list( ids: Sequence[str] | None = None, query: ApplicationIdsQuery | None = None, ) -> ApplicationsResult | ApplicationIdsResult: + """Выполняет публичную операцию `Application.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + client = ApplicationsClient(self.transport) if ids is not None: return client.get_by_ids(ids=list(ids)) @@ -172,6 +261,13 @@ def list( return client.get_ids(query=query) def get_states(self) -> ApplicationStatesResult: + """Выполняет публичную операцию `Application.get_states` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ApplicationsClient(self.transport).get_states() def update( @@ -180,6 +276,15 @@ def update( applies: Sequence[ApplicationViewedItem], idempotency_key: str | None = None, ) -> JobActionResult: + """Выполняет публичную операцию `Application.update` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ApplicationsClient(self.transport).set_is_viewed( applies=list(applies), idempotency_key=idempotency_key, @@ -194,14 +299,35 @@ class Resume(DomainObject): user_id: int | str | None = None def list(self, *, query: ResumeSearchQuery | None = None) -> ResumesResult: + """Выполняет публичную операцию `Resume.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ResumeClient(self.transport).search(query=query) def get(self, *, resume_id: int | str | None = None) -> ResumeInfo: + """Выполняет публичную операцию `Resume.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ResumeClient(self.transport).get_item( resume_id=str(resume_id or self._require_resume_id()) ) def get_contacts(self, *, resume_id: int | str | None = None) -> ResumeContactInfo: + """Выполняет публичную операцию `Resume.get_contacts` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ResumeClient(self.transport).get_contacts( resume_id=str(resume_id or self._require_resume_id()) ) @@ -219,14 +345,35 @@ class JobWebhook(DomainObject): user_id: int | str | None = None def get(self) -> JobWebhookInfo: + """Выполняет публичную операцию `JobWebhook.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return WebhookClient(self.transport).get_webhook() def list(self) -> JobWebhooksResult: + """Выполняет публичную операцию `JobWebhook.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return WebhookClient(self.transport).list_webhooks() - def update( - self, *, url: str, idempotency_key: str | None = None - ) -> JobWebhookInfo: + def update(self, *, url: str, idempotency_key: str | None = None) -> JobWebhookInfo: + """Выполняет публичную операцию `JobWebhook.update` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return WebhookClient(self.transport).put_webhook( url=url, idempotency_key=idempotency_key, @@ -235,6 +382,15 @@ def update( def delete( self, *, url: str | None = None, idempotency_key: str | None = None ) -> JobActionResult: + """Выполняет публичную операцию `JobWebhook.delete` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return WebhookClient(self.transport).delete_webhook( url=url, idempotency_key=idempotency_key, @@ -249,9 +405,23 @@ class JobDictionary(DomainObject): user_id: int | str | None = None def list(self) -> JobDictionariesResult: + """Выполняет публичную операцию `JobDictionary.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return DictionariesClient(self.transport).list_dicts() def get(self, *, dictionary_id: str | None = None) -> JobDictionaryValuesResult: + """Выполняет публичную операцию `JobDictionary.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return DictionariesClient(self.transport).get_dict_by_id( dictionary_id=dictionary_id or self._require_dictionary_id() ) diff --git a/avito/messenger/domain.py b/avito/messenger/domain.py index 6a5b97f..b21a7b8 100644 --- a/avito/messenger/domain.py +++ b/avito/messenger/domain.py @@ -32,7 +32,10 @@ class Chat(DomainObject): user_id: int | str | None = None def get(self) -> ChatInfo: - """Получает чат по `chat_id`.""" + """Получает чат по `chat_id`. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MessengerClient(self.transport).get_chat( user_id=self._require_user_id(), @@ -40,12 +43,22 @@ def get(self) -> ChatInfo: ) def list(self) -> ChatsResult: - """Получает список чатов пользователя.""" + """Получает список чатов пользователя. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MessengerClient(self.transport).list_chats(user_id=self._require_user_id()) def mark_read(self, *, idempotency_key: str | None = None) -> MessageActionResult: - """Помечает чат как прочитанный.""" + """Помечает чат как прочитанный. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MessengerClient(self.transport).read_chat( user_id=self._require_user_id(), @@ -59,7 +72,12 @@ def blacklist( blacklisted_user_id: int, idempotency_key: str | None = None, ) -> MessageActionResult: - """Добавляет пользователя в blacklist.""" + """Добавляет пользователя в blacklist. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MessengerClient(self.transport).add_to_blacklist( user_id=self._require_user_id(), @@ -87,7 +105,12 @@ class ChatMessage(DomainObject): user_id: int | str | None = None def list(self, *, chat_id: str | None = None) -> MessagesResult: - """Получает список сообщений V3.""" + """Получает список сообщений V3. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MessengerClient(self.transport).list_messages( user_id=self._require_user_id(), @@ -101,7 +124,12 @@ def send_message( message: str, idempotency_key: str | None = None, ) -> MessageActionResult: - """Отправляет текстовое сообщение.""" + """Отправляет текстовое сообщение. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MessengerClient(self.transport).send_message( user_id=self._require_user_id(), @@ -118,7 +146,12 @@ def send_image( caption: str | None = None, idempotency_key: str | None = None, ) -> MessageActionResult: - """Отправляет сообщение с изображением.""" + """Отправляет сообщение с изображением. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MessengerClient(self.transport).send_image_message( user_id=self._require_user_id(), @@ -135,7 +168,12 @@ def delete( message_id: str | None = None, idempotency_key: str | None = None, ) -> MessageActionResult: - """Удаляет сообщение.""" + """Удаляет сообщение. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_message_id = message_id or self._require_message_id() return MessengerClient(self.transport).delete_message( @@ -168,12 +206,22 @@ class ChatWebhook(DomainObject): user_id: int | str | None = None def list(self) -> SubscriptionsResult: - """Получает список webhook-подписок.""" + """Получает список webhook-подписок. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return WebhookClient(self.transport).get_subscriptions() def unsubscribe(self, *, url: str, idempotency_key: str | None = None) -> WebhookActionResult: - """Отключает webhook.""" + """Отключает webhook. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return WebhookClient(self.transport).unsubscribe(url=url, idempotency_key=idempotency_key) @@ -184,7 +232,12 @@ def subscribe( secret: str | None = None, idempotency_key: str | None = None, ) -> WebhookActionResult: - """Включает webhook v3.""" + """Включает webhook v3. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return WebhookClient(self.transport).update_v3( url=url, @@ -200,7 +253,10 @@ class ChatMedia(DomainObject): user_id: int | str | None = None def get_voice_files(self) -> VoiceFilesResult: - """Получает голосовые сообщения.""" + """Получает голосовые сообщения. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MediaClient(self.transport).get_voice_files(user_id=self._require_user_id()) @@ -210,7 +266,12 @@ def upload_images( files: list[UploadImageFile], idempotency_key: str | None = None, ) -> UploadImagesResult: - """Загружает изображения для сообщений.""" + """Загружает изображения для сообщений. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return MediaClient(self.transport).upload_images( user_id=self._require_user_id(), @@ -232,7 +293,10 @@ class SpecialOfferCampaign(DomainObject): user_id: int | str | None = None def get_available(self, *, item_ids: list[int]) -> SpecialOfferAvailableResult: - """Получает объявления, доступные для рассылки.""" + """Получает объявления, доступные для рассылки. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return SpecialOffersClient(self.transport).get_available(item_ids=item_ids) @@ -244,7 +308,12 @@ def create_multi( discount_percent: int | None = None, idempotency_key: str | None = None, ) -> MultiCreateSpecialOfferResult: - """Создает рассылку спецпредложений.""" + """Создает рассылку спецпредложений. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return SpecialOffersClient(self.transport).create_multi( item_ids=item_ids, @@ -259,7 +328,12 @@ def confirm_multi( campaign_id: str | None = None, idempotency_key: str | None = None, ) -> WebhookActionResult: - """Подтверждает и оплачивает рассылку.""" + """Подтверждает и оплачивает рассылку. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return SpecialOffersClient(self.transport).confirm_multi( campaign_id=campaign_id or self._require_campaign_id(), @@ -267,14 +341,20 @@ def confirm_multi( ) def get_stats(self, *, campaign_id: str | None = None) -> SpecialOfferStatsResult: - """Получает статистику рассылки.""" + """Получает статистику рассылки. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return SpecialOffersClient(self.transport).get_stats( campaign_id=campaign_id or self._require_campaign_id() ) def get_tariff_info(self) -> TariffInfo: - """Получает информацию о тарифе спецпредложений.""" + """Получает информацию о тарифе спецпредложений. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return SpecialOffersClient(self.transport).get_tariff_info() diff --git a/avito/orders/domain.py b/avito/orders/domain.py index 678c0db..7aa5987 100644 --- a/avito/orders/domain.py +++ b/avito/orders/domain.py @@ -56,11 +56,27 @@ class Order(DomainObject): user_id: int | str | None = None def list(self) -> OrdersResult: + """Выполняет публичную операцию `Order.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).list_orders() def update_markings( self, *, order_id: str, codes: Sequence[str], idempotency_key: str | None = None ) -> OrderActionResult: + """Выполняет публичную операцию `Order.update_markings` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).update_markings( order_id=order_id, codes=list(codes), @@ -70,6 +86,15 @@ def update_markings( def accept_return_order( self, *, order_id: str, postal_office_id: str, idempotency_key: str | None = None ) -> OrderActionResult: + """Выполняет публичную операцию `Order.accept_return_order` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).accept_return_order( order_id=order_id, postal_office_id=postal_office_id, @@ -79,6 +104,15 @@ def accept_return_order( def apply( self, *, order_id: str, transition: str, idempotency_key: str | None = None ) -> OrderActionResult: + """Выполняет публичную операцию `Order.apply` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).apply_transition( order_id=order_id, transition=transition, @@ -88,6 +122,15 @@ def apply( def check_confirmation_code( self, *, order_id: str, code: str, idempotency_key: str | None = None ) -> OrderActionResult: + """Выполняет публичную операцию `Order.check_confirmation_code` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).check_confirmation_code( order_id=order_id, code=code, @@ -97,6 +140,15 @@ def check_confirmation_code( def set_cnc_details( self, *, order_id: str, pickup_point_id: str, idempotency_key: str | None = None ) -> OrderActionResult: + """Выполняет публичную операцию `Order.set_cnc_details` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).set_cnc_details( order_id=order_id, pickup_point_id=pickup_point_id, @@ -104,11 +156,27 @@ def set_cnc_details( ) def get_courier_delivery_range(self) -> CourierRangesResult: + """Выполняет публичную операцию `Order.get_courier_delivery_range` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).get_courier_delivery_range() def set_courier_delivery_range( self, *, order_id: str, interval_id: str, idempotency_key: str | None = None ) -> OrderActionResult: + """Выполняет публичную операцию `Order.set_courier_delivery_range` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).set_courier_delivery_range( order_id=order_id, interval_id=interval_id, @@ -118,6 +186,15 @@ def set_courier_delivery_range( def update_tracking_number( self, *, order_id: str, tracking_number: str, idempotency_key: str | None = None ) -> OrderActionResult: + """Выполняет публичную операцию `Order.update_tracking_number` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return OrdersClient(self.transport).set_tracking_number( order_id=order_id, tracking_number=tracking_number, @@ -139,6 +216,15 @@ def create( extended: bool = False, idempotency_key: str | None = None, ) -> LabelTaskResult: + """Выполняет публичную операцию `OrderLabel.create` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + client = LabelsClient(self.transport) if extended: return client.create_generate_labels_extended( @@ -151,6 +237,13 @@ def create( ) def download(self, *, task_id: str | None = None) -> LabelPdfResult: + """Выполняет публичную операцию `OrderLabel.download` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + resolved_task_id = task_id or self._require_task_id() return LabelsClient(self.transport).get_download_label(task_id=resolved_task_id) @@ -169,14 +262,30 @@ class DeliveryOrder(DomainObject): def create_announcement( self, *, order_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `DeliveryOrder.create_announcement` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return DeliveryClient(self.transport).create_announcement( order_id=order_id, idempotency_key=idempotency_key, ) - def delete( - self, *, order_id: str, idempotency_key: str | None = None - ) -> DeliveryEntityResult: + def delete(self, *, order_id: str, idempotency_key: str | None = None) -> DeliveryEntityResult: + """Выполняет публичную операцию `DeliveryOrder.delete` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return DeliveryClient(self.transport).cancel_announcement( order_id=order_id, idempotency_key=idempotency_key, @@ -189,6 +298,15 @@ def create( parcel_id: str, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `DeliveryOrder.create` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return DeliveryClient(self.transport).create_parcel( order_id=order_id, parcel_id=parcel_id, @@ -198,6 +316,15 @@ def create( def update_change_parcels( self, *, parcel_ids: Sequence[str], idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `DeliveryOrder.update_change_parcels` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return DeliveryClient(self.transport).update_change_parcels( parcel_ids=list(parcel_ids), idempotency_key=idempotency_key, @@ -206,6 +333,15 @@ def update_change_parcels( def create_change_parcel_result( self, *, parcel_id: str, result: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `DeliveryOrder.create_change_parcel_result` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return DeliveryClient(self.transport).change_parcel_result( parcel_id=parcel_id, result=result, @@ -222,6 +358,15 @@ class SandboxDelivery(DomainObject): def create_announcement( self, *, order_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.create_announcement` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).create_announcement( order_id=order_id, idempotency_key=idempotency_key, @@ -230,6 +375,15 @@ def create_announcement( def track_announcement( self, *, order_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.track_announcement` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).track_announcement( order_id=order_id, idempotency_key=idempotency_key, @@ -238,6 +392,15 @@ def track_announcement( def update_custom_area_schedule( self, *, items: Sequence[CustomAreaScheduleEntry], idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.update_custom_area_schedule` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).update_custom_area_schedule( items=list(items), idempotency_key=idempotency_key, @@ -246,6 +409,15 @@ def update_custom_area_schedule( def cancel_parcel( self, *, parcel_id: str, actor: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.cancel_parcel` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).cancel_parcel( parcel_id=parcel_id, actor=actor, @@ -255,6 +427,15 @@ def cancel_parcel( def check_confirmation_code( self, *, parcel_id: str, confirm_code: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.check_confirmation_code` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).check_confirmation_code( parcel_id=parcel_id, confirm_code=confirm_code, @@ -268,6 +449,15 @@ def set_order_properties( properties: OrderDeliveryProperties, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.set_order_properties` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).set_order_properties( order_id=order_id, properties=properties, @@ -277,6 +467,15 @@ def set_order_properties( def set_order_real_address( self, *, order_id: str, address: RealAddress, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.set_order_real_address` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).set_order_real_address( order_id=order_id, address=address, @@ -296,6 +495,15 @@ def tracking( options: DeliveryTrackingOptions | None = None, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.tracking` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).tracking( order_id=order_id, avito_status=avito_status, @@ -311,17 +519,42 @@ def tracking( def prohibit_order_acceptance( self, *, order_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.prohibit_order_acceptance` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).prohibit_order_acceptance( order_id=order_id, idempotency_key=idempotency_key, ) def list_sorting_center(self) -> DeliverySortingCentersResult: + """Выполняет публичную операцию `SandboxDelivery.list_sorting_center` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).list_sorting_center() def add_sorting_center( self, *, items: Sequence[SortingCenterUpload], idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.add_sorting_center` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).add_sorting_center( items=list(items), idempotency_key=idempotency_key, @@ -334,6 +567,15 @@ def add_areas( areas: Sequence[SandboxArea], idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.add_areas` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).add_areas( tariff_id=tariff_id, areas=list(areas), @@ -347,6 +589,15 @@ def add_tags_to_sorting_center( items: Sequence[TaggedSortingCenter], idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.add_tags_to_sorting_center` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).add_tags_to_sorting_center( tariff_id=tariff_id, items=list(items), @@ -360,6 +611,15 @@ def add_terminals( items: Sequence[TerminalUpload], idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.add_terminals` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).add_terminals( tariff_id=tariff_id, items=list(items), @@ -373,6 +633,15 @@ def update_terms( items: Sequence[DeliveryTermsZone], idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.update_terms` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).update_terms( tariff_id=tariff_id, items=list(items), @@ -390,6 +659,15 @@ def add_tariff( tariff_type: str | None = None, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.add_tariff` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).add_tariff( name=name, delivery_provider_tariff_id=delivery_provider_tariff_id, @@ -407,6 +685,15 @@ def create_parcel( parcel_id: str, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.create_parcel` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).create_parcel( order_id=order_id, parcel_id=parcel_id, @@ -421,6 +708,15 @@ def cancel_sandbox_announcement( options: SandboxCancelAnnouncementOptions, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.cancel_sandbox_announcement` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).cancel_sandbox_announcement( announcement_id=announcement_id, date=date, @@ -435,6 +731,15 @@ def cancel_sandbox_parcel( options: CancelSandboxParcelOptions | None = None, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.cancel_sandbox_parcel` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).cancel_sandbox_parcel( parcel_id=parcel_id, options=options, @@ -450,6 +755,15 @@ def change_sandbox_parcel( options: ChangeParcelOptions | None = None, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.change_sandbox_parcel` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).change_sandbox_parcel( type=type, parcel_id=parcel_id, @@ -471,6 +785,15 @@ def create_sandbox_announcement( options: SandboxCreateAnnouncementOptions, idempotency_key: str | None = None, ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.create_sandbox_announcement` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).create_sandbox_announcement( announcement_id=announcement_id, barcode=barcode, @@ -486,6 +809,15 @@ def create_sandbox_announcement( def get_sandbox_announcement_event( self, *, announcement_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.get_sandbox_announcement_event` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).get_sandbox_announcement_event( announcement_id=announcement_id, idempotency_key=idempotency_key, @@ -494,6 +826,15 @@ def get_sandbox_announcement_event( def get_sandbox_change_parcel_info( self, *, application_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.get_sandbox_change_parcel_info` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).get_sandbox_change_parcel_info( application_id=application_id, idempotency_key=idempotency_key, @@ -502,6 +843,15 @@ def get_sandbox_change_parcel_info( def get_sandbox_parcel_info( self, *, parcel_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.get_sandbox_parcel_info` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).get_sandbox_parcel_info( parcel_id=parcel_id, idempotency_key=idempotency_key, @@ -510,6 +860,15 @@ def get_sandbox_parcel_info( def get_sandbox_registered_parcel_id( self, *, order_id: str, idempotency_key: str | None = None ) -> DeliveryEntityResult: + """Выполняет публичную операцию `SandboxDelivery.get_sandbox_registered_parcel_id` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return SandboxDeliveryClient(self.transport).get_sandbox_registered_parcel_id( order_id=order_id, idempotency_key=idempotency_key, @@ -524,6 +883,13 @@ class DeliveryTask(DomainObject): user_id: int | str | None = None def get(self, *, task_id: str | None = None) -> DeliveryTaskInfo: + """Выполняет публичную операцию `DeliveryTask.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + resolved_task_id = task_id or self._require_task_id() return DeliveryTasksClient(self.transport).get_task(task_id=resolved_task_id) @@ -540,6 +906,13 @@ class Stock(DomainObject): user_id: int | str | None = None def get(self, *, item_ids: Sequence[int]) -> StockInfoResult: + """Выполняет публичную операцию `Stock.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return StockManagementClient(self.transport).get_info(item_ids=list(item_ids)) def update( @@ -548,6 +921,15 @@ def update( stocks: Sequence[StockUpdateEntry], idempotency_key: str | None = None, ) -> StockUpdateResult: + """Выполняет публичную операцию `Stock.update` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return StockManagementClient(self.transport).update_stocks( stocks=list(stocks), idempotency_key=idempotency_key, diff --git a/avito/promotion/domain.py b/avito/promotion/domain.py index 216208e..bdec895 100644 --- a/avito/promotion/domain.py +++ b/avito/promotion/domain.py @@ -85,16 +85,22 @@ class PromotionOrder(DomainObject): order_id: int | str | None = None def get_service_dictionary(self) -> PromotionServiceDictionary: - """Получает словарь услуг продвижения.""" + """Получает словарь услуг продвижения. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return PromotionClient(self.transport).get_service_dictionary() def list_services(self, *, item_ids: list[int]) -> PromotionServicesResult: - """Получает список услуг продвижения по объявлениям.""" + """Получает список услуг продвижения по объявлениям. - return PromotionClient(self.transport).list_services( - item_ids=item_ids - ) + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + + return PromotionClient(self.transport).list_services(item_ids=item_ids) def list_orders( self, @@ -102,7 +108,12 @@ def list_orders( item_ids: list[int] | None = None, order_ids: list[str] | None = None, ) -> PromotionOrdersResult: - """Получает список заявок на продвижение.""" + """Получает список заявок на продвижение. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return PromotionClient(self.transport).list_orders( item_ids=item_ids, @@ -110,16 +121,17 @@ def list_orders( ) def get_order_status(self, *, order_ids: list[str] | None = None) -> PromotionOrderStatusResult: - """Получает статусы заявок на продвижение.""" + """Получает статусы заявок на продвижение. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_order_ids = order_ids or ( [str(self.order_id)] if self.order_id is not None else [] ) if not resolved_order_ids: raise ValidationError("Для операции требуется хотя бы один `order_id`.") - return PromotionClient(self.transport).get_order_status( - order_ids=resolved_order_ids - ) + return PromotionClient(self.transport).get_order_status(order_ids=resolved_order_ids) @dataclass(slots=True, frozen=True) @@ -130,7 +142,10 @@ class BbipPromotion(DomainObject): user_id: int | str | None = None def get_forecasts(self, *, items: list[BbipItemInput]) -> BbipForecastsResult: - """Получает прогнозы BBIP.""" + """Получает прогнозы BBIP. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ bbip_items = [ BbipItem( @@ -150,7 +165,14 @@ def create_order( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Подключает BBIP-продвижение.""" + """Подключает BBIP-продвижение. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ validate_non_empty("items", items) for index, item in enumerate(items): @@ -181,12 +203,13 @@ def create_order( ) def get_suggests(self, *, item_ids: list[int] | None = None) -> BbipSuggestsResult: - """Получает варианты бюджета BBIP.""" + """Получает варианты бюджета BBIP. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_item_ids = item_ids or self._resource_item_ids() - return BbipClient(self.transport).get_suggests( - item_ids=resolved_item_ids - ) + return BbipClient(self.transport).get_suggests(item_ids=resolved_item_ids) def _resource_item_ids(self) -> list[int]: if self.item_id is None: @@ -208,7 +231,14 @@ def apply( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Запускает TrxPromo.""" + """Запускает TrxPromo. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ validate_non_empty("items", items) for index, item in enumerate(items): @@ -241,7 +271,14 @@ def delete( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Останавливает TrxPromo.""" + """Останавливает TrxPromo. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_item_ids = item_ids or self._resource_item_ids() validate_non_empty("item_ids", resolved_item_ids) @@ -255,7 +292,10 @@ def delete( ) def get_commissions(self, *, item_ids: list[int] | None = None) -> TrxCommissionsResult: - """Получает доступные комиссии TrxPromo.""" + """Получает доступные комиссии TrxPromo. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return TrxPromoClient(self.transport).get_commissions( item_ids=item_ids or self._resource_item_ids() @@ -279,7 +319,10 @@ def get_user_bids( from_item_id: int | None = None, batch_size: int | None = None, ) -> CpaAuctionBidsResult: - """Получает действующие и доступные ставки.""" + """Получает действующие и доступные ставки. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return CpaAuctionClient(self.transport).get_user_bids( from_item_id=from_item_id, @@ -292,9 +335,17 @@ def create_item_bids( items: list[BidItemInput], idempotency_key: str | None = None, ) -> PromotionActionResult: - """Сохраняет новые ставки по объявлениям.""" + """Сохраняет новые ставки по объявлениям. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ - bids = [CreateItemBid(item_id=item["item_id"], price_penny=item["price_penny"]) for item in items] + bids = [ + CreateItemBid(item_id=item["item_id"], price_penny=item["price_penny"]) + for item in items + ] return CpaAuctionClient(self.transport).create_item_bids( items=bids, idempotency_key=idempotency_key, @@ -309,7 +360,10 @@ class TargetActionPricing(DomainObject): user_id: int | str | None = None def get_bids(self, *, item_id: int | None = None) -> TargetActionGetBidsResult: - """Получает детализированные цены и бюджеты.""" + """Получает детализированные цены и бюджеты. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return TargetActionPriceClient(self.transport).get_bids( item_id=item_id or self._require_item_id() @@ -318,7 +372,10 @@ def get_bids(self, *, item_id: int | None = None) -> TargetActionGetBidsResult: def get_promotions_by_item_ids( self, *, item_ids: list[int] | None = None ) -> TargetActionPromotionsByItemIdsResult: - """Получает текущие настройки по нескольким объявлениям.""" + """Получает текущие настройки по нескольким объявлениям. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_item_ids = item_ids or [self._require_item_id()] return TargetActionPriceClient(self.transport).get_promotions_by_item_ids( @@ -332,7 +389,14 @@ def delete( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Останавливает продвижение.""" + """Останавливает продвижение. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_item_id = item_id or self._require_item_id() validate_positive_int("item_id", resolved_item_id) @@ -355,7 +419,14 @@ def update_auto( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Применяет автоматическую настройку.""" + """Применяет автоматическую настройку. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_item_id = item_id or self._require_item_id() validate_positive_int("item_id", resolved_item_id) @@ -393,7 +464,14 @@ def update_manual( dry_run: bool = False, idempotency_key: str | None = None, ) -> PromotionActionResult: - """Применяет ручную настройку.""" + """Применяет ручную настройку. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + При `dry_run=True` payload строится без вызова транспорта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ resolved_item_id = item_id or self._require_item_id() validate_positive_int("item_id", resolved_item_id) @@ -443,7 +521,10 @@ def create_budget( finish_time: datetime | None = None, items: list[int] | None = None, ) -> AutostrategyBudget: - """Рассчитывает бюджет кампании.""" + """Рассчитывает бюджет кампании. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ _validate_optional_datetime("start_time", start_time) _validate_optional_datetime("finish_time", finish_time) @@ -469,7 +550,12 @@ def create( start_time: datetime | None = None, idempotency_key: str | None = None, ) -> CampaignActionResult: - """Создает новую кампанию.""" + """Создает новую кампанию. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ _validate_optional_datetime("start_time", start_time) _validate_optional_datetime("finish_time", finish_time) @@ -501,7 +587,12 @@ def update( title: str | None = None, idempotency_key: str | None = None, ) -> CampaignActionResult: - """Редактирует кампанию.""" + """Редактирует кампанию. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ _validate_optional_datetime("start_time", start_time) _validate_optional_datetime("finish_time", finish_time) @@ -519,7 +610,10 @@ def update( ) def get(self, *, campaign_id: int | None = None) -> CampaignDetailsResult: - """Получает полную информацию о кампании.""" + """Получает полную информацию о кампании. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutostrategyClient(self.transport).get_campaign_info( campaign_id=campaign_id or self._require_campaign_id() @@ -532,7 +626,12 @@ def delete( campaign_id: int | None = None, idempotency_key: str | None = None, ) -> CampaignActionResult: - """Останавливает кампанию.""" + """Останавливает кампанию. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutostrategyClient(self.transport).stop_campaign( campaign_id=campaign_id or self._require_campaign_id(), @@ -550,7 +649,12 @@ def list( updated_from: datetime | None = None, updated_to: datetime | None = None, ) -> CampaignsResult: - """Получает список кампаний.""" + """Получает список кампаний. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ filter_payload = ( CampaignListFilter( @@ -576,7 +680,10 @@ def list( ) def get_stat(self, *, campaign_id: int | None = None) -> AutostrategyStat: - """Получает статистику кампании.""" + """Получает статистику кампании. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return AutostrategyClient(self.transport).get_stat( campaign_id=campaign_id or self._require_campaign_id() diff --git a/avito/ratings/domain.py b/avito/ratings/domain.py index 7ea7dab..8ce8ad6 100644 --- a/avito/ratings/domain.py +++ b/avito/ratings/domain.py @@ -22,6 +22,13 @@ class Review(DomainObject): user_id: int | str | None = None def list(self, *, query: ReviewsQuery | None = None) -> ReviewsResult: + """Выполняет публичную операцию `Review.list` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return RatingsClient(self.transport).list_reviews(query=query) @@ -39,6 +46,15 @@ def create( text: str, idempotency_key: str | None = None, ) -> ReviewAnswerInfo: + """Выполняет публичную операцию `ReviewAnswer.create` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return RatingsClient(self.transport).create_review_answer( review_id=review_id, text=text, @@ -51,6 +67,15 @@ def delete( answer_id: int | str | None = None, idempotency_key: str | None = None, ) -> ReviewAnswerInfo: + """Выполняет публичную операцию `ReviewAnswer.delete` и возвращает типизированную SDK-модель. + + Параметр `idempotency_key` задает ключ идемпотентности для безопасного повтора write-операции. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return RatingsClient(self.transport).delete_review_answer( answer_id=answer_id or self._require_answer_id(), idempotency_key=idempotency_key, @@ -69,6 +94,13 @@ class RatingProfile(DomainObject): user_id: int | str | None = None def get(self) -> RatingProfileInfo: + """Выполняет публичную операцию `RatingProfile.get` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return RatingsClient(self.transport).get_ratings_info() diff --git a/avito/realty/domain.py b/avito/realty/domain.py index 873a0e6..74ec801 100644 --- a/avito/realty/domain.py +++ b/avito/realty/domain.py @@ -31,6 +31,13 @@ def get_intervals( intervals: list[RealtyInterval], item_id: int | None = None, ) -> RealtyActionResult: + """Выполняет публичную операцию `RealtyListing.get_intervals` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ShortTermRentClient(self.transport).get_intervals( item_id=item_id or int(self._require_item_id()), intervals=intervals, @@ -39,6 +46,13 @@ def get_intervals( def update_base_params( self, *, min_stay_days: int, item_id: int | str | None = None ) -> RealtyActionResult: + """Выполняет публичную операцию `RealtyListing.update_base_params` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ShortTermRentClient(self.transport).update_base_params( item_id=item_id or self._require_item_id(), min_stay_days=min_stay_days, @@ -64,6 +78,13 @@ def update_bookings_info( user_id: int | str | None = None, item_id: int | str | None = None, ) -> RealtyActionResult: + """Выполняет публичную операцию `RealtyBooking.update_bookings_info` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ShortTermRentClient(self.transport).update_bookings_info( user_id=user_id or self._require_user_id(), item_id=item_id or self._require_item_id(), @@ -79,6 +100,13 @@ def list_realty_bookings( user_id: int | str | None = None, item_id: int | str | None = None, ) -> RealtyBookingsResult: + """Выполняет публичную операцию `RealtyBooking.list_realty_bookings` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ShortTermRentClient(self.transport).list_realty_bookings( user_id=user_id or self._require_user_id(), item_id=item_id or self._require_item_id(), @@ -114,6 +142,13 @@ def update_realty_prices( user_id: int | str | None = None, item_id: int | str | None = None, ) -> RealtyActionResult: + """Выполняет публичную операцию `RealtyPricing.update_realty_prices` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return ShortTermRentClient(self.transport).update_realty_prices( user_id=user_id or self._require_user_id(), item_id=item_id or self._require_item_id(), @@ -144,12 +179,26 @@ def get_market_price_correspondence( item_id: int | str | None = None, price: int | str, ) -> RealtyMarketPriceInfo: + """Выполняет публичную операцию `RealtyAnalyticsReport.get_market_price_correspondence` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return RealtyAnalyticsClient(self.transport).get_market_price_correspondence( item_id=item_id or self._require_item_id(), price=price, ) def get_report_for_classified(self, *, item_id: int | str | None = None) -> RealtyAnalyticsInfo: + """Выполняет публичную операцию `RealtyAnalyticsReport.get_report_for_classified` и возвращает типизированную SDK-модель. + + Пустой результат возвращается как пустая коллекция или `None` согласно аннотации метода. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ + return RealtyAnalyticsClient(self.transport).get_report_for_classified( item_id=item_id or self._require_item_id() ) diff --git a/avito/tariffs/domain.py b/avito/tariffs/domain.py index 0f73a30..5296f69 100644 --- a/avito/tariffs/domain.py +++ b/avito/tariffs/domain.py @@ -16,7 +16,10 @@ class Tariff(DomainObject): tariff_id: int | str | None = None def get_tariff_info(self) -> TariffInfo: - """Получает информацию о тарифе аккаунта.""" + """Получает информацию о тарифе аккаунта. + + Raises: AvitoError с полями operation, status, request_id, attempt, method и endpoint. + """ return TariffsClient(self.transport).get_tariff_info() diff --git a/docs/site/assets/_gen_reference.py b/docs/site/assets/_gen_reference.py index eb5969c..97c910d 100644 --- a/docs/site/assets/_gen_reference.py +++ b/docs/site/assets/_gen_reference.py @@ -8,6 +8,7 @@ import mkdocs_gen_files from scripts.parse_inventory import InventoryRow, parse_inventory +from scripts.public_sdk_surface import public_method_name EXCLUDED_PACKAGES = {"auth", "core", "testing"} @@ -69,7 +70,8 @@ def write_operations(rows: list[InventoryRow]) -> None: "|---|---|---|---|---|\n" ) for row in rows: - sdk = f"`avito.{row.sdk_package}.{row.domain_object}.{row.sdk_public_method}()`" + method_name = public_method_name(row) + sdk = f"`avito.{row.sdk_package}.{row.domain_object}.{method_name}()`" http = f"`{row.method} {row.path}`" deprecated = "нет" if row.deprecated: diff --git a/docs/site/reference/testing.md b/docs/site/reference/testing.md index 387a6b8..6335d05 100644 --- a/docs/site/reference/testing.md +++ b/docs/site/reference/testing.md @@ -12,6 +12,8 @@ - `FakeTransport` записывает выполненные запросы и отдаёт заранее заданные ответы. - `json_response()` создаёт JSON-ответ для маршрута. - `route_sequence()` задаёт последовательность ответов для retry и stateful-сценариев. +- `FakeTransport.as_client()` создаёт полностью инициализированный `AvitoClient` + поверх fake transport без реального HTTP. - `RecordedRequest` позволяет проверять method, path, query params, headers и JSON body. Пользовательские тесты должны работать через публичные утилиты `avito.testing`, diff --git a/poetry.lock b/poetry.lock index d1c20d5..5dc2ed4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -53,6 +53,31 @@ files = [ [package.extras] extras = ["regex"] +[[package]] +name = "bandit" +version = "1.9.4" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e"}, + {file = "bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] +yaml = ["PyYAML"] + [[package]] name = "bracex" version = "2.6" @@ -617,6 +642,30 @@ files = [ docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + [[package]] name = "markupsafe" version = "3.0.3" @@ -716,6 +765,18 @@ files = [ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mergedeep" version = "1.3.4" @@ -1405,6 +1466,25 @@ files = [ [package.dependencies] httpx = ">=0.25.0" +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["docs"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.12.12" @@ -1446,6 +1526,18 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "stevedore" +version = "5.7.0" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed"}, + {file = "stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1552,4 +1644,4 @@ bracex = ">=2.1.1" [metadata] lock-version = "2.1" python-versions = "^3.14" -content-hash = "a96e13b2eced596d3c3cc912c1fde282cac5cb510c9dfa0b71e5d2b37bda2b84" +content-hash = "9d93e82c7bbd2549c9184ae2dcb60760017ee6281063865726e5efd92e634565" diff --git a/pyproject.toml b/pyproject.toml index 8de85bc..287e4a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ mkdocstrings = { version = ">=0.27", extras = ["python"] } mkdocs-gen-files = ">=0.5" mkdocs-literate-nav = ">=0.6" mktestdocs = "^0.2.5" +bandit = ">=1.7" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scripts/build_docs_quality_report.py b/scripts/build_docs_quality_report.py index c198ae7..b1c206d 100644 --- a/scripts/build_docs_quality_report.py +++ b/scripts/build_docs_quality_report.py @@ -2,6 +2,7 @@ import argparse import json +import os import re import tomllib from datetime import UTC, datetime @@ -39,11 +40,33 @@ def read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) +def read_text(path: Path) -> str: + if not path.exists(): + return "" + return path.read_text(encoding="utf-8") + + def sdk_version() -> str: payload = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) return str(payload["tool"]["poetry"]["version"]) +def ttfc_minutes(args: argparse.Namespace) -> float | None: + if args.ttfc_minutes is not None: + return args.ttfc_minutes + env_value = os.environ.get("TTFC_MINUTES") + if env_value: + return float(env_value) + path = ROOT / "ttfc-minutes.txt" + if path.exists(): + return float(path.read_text(encoding="utf-8").strip()) + return None + + +def semver_is_valid(version: str) -> bool: + return re.fullmatch(r"0|[1-9]\d*\.(0|[1-9]\d*)\.(0|[1-9]\d*)", version) is not None + + def markdown_files(section: str) -> list[str]: directory = DOCS_DIR / section if not directory.exists(): @@ -67,6 +90,90 @@ def docs_examples_harness_enabled() -> bool: ) +def pr_template_has_public_rename_gate() -> bool: + path = ROOT / ".github" / "pull_request_template.md" + text = read_text(path) + return "Публичное переименование" in text and "DeprecationWarning" in text + + +def debug_info_contract_is_documented() -> bool: + client_reference = read_text(DOCS_DIR / "reference" / "client.md") + security_explanation = read_text(DOCS_DIR / "explanations" / "security-and-redaction.md") + client_tests = read_text(ROOT / "tests" / "contracts" / "test_client_contracts.py") + required = ("debug_info", "client_secret", "Authorization", "secret") + return ( + all(marker in client_reference + security_explanation for marker in required[:3]) + and "test_debug_info_and_context_manager_do_not_leak_secrets" in client_tests + and "secret" in client_tests + ) + + +def testing_contract_is_documented() -> bool: + reference = read_text(DOCS_DIR / "reference" / "testing.md") + explanation = read_text(DOCS_DIR / "explanations" / "testing-strategy.md") + tests = read_text(ROOT / "tests" / "contracts" / "test_testing_api.py") + text = reference + explanation + return ( + "FakeTransport" in text + and "route_sequence" in text + and "RecordedRequest" in text + and "as_client" in text + and "test_fake_transport_builds_public_client_without_real_http" in tests + ) + + +def serialization_contract_is_documented() -> bool: + reference = read_text(DOCS_DIR / "reference" / "models.md") + explanation = read_text(DOCS_DIR / "explanations" / "security-and-redaction.md") + tests = read_text(ROOT / "tests" / "contracts" / "test_model_contracts.py") + return ( + "to_dict()" in reference + and "model_dump()" in reference + and "JSON-совмест" in reference + and "to_dict()" in explanation + and "test_recursive_serialization_is_json_compatible" in tests + ) + + +def context_manager_contract_is_documented() -> bool: + reference = read_text(DOCS_DIR / "reference" / "client.md") + tutorial = read_text(DOCS_DIR / "tutorials" / "getting-started.md") + tests = read_text(ROOT / "tests" / "contracts" / "test_client_contracts.py") + return ( + "context manager" in reference + and "close()" in reference + and "ConfigurationError" in reference + and "with AvitoClient.from_env()" in tutorial + and "test_closed_client_rejects_new_domain_factories" in tests + ) + + +def deprecation_warning_contract_is_tested() -> bool: + tests = read_text(ROOT / "tests" / "contracts" / "test_deprecation_warnings.py") + changelog = read_text(ROOT / "CHANGELOG.md") + return ( + "test_deprecated_inventory_symbols_warn_once" in tests + and "DeprecationWarning" in tests + and "DeprecationWarning" in changelog + ) + + +def bandit_high_count(report: dict[str, Any]) -> int: + metrics = report.get("metrics") + if isinstance(metrics, dict): + totals = metrics.get("_totals") + if isinstance(totals, dict) and isinstance(totals.get("SEVERITY.HIGH"), int): + return int(totals["SEVERITY.HIGH"]) + results = report.get("results") + if not isinstance(results, list): + return 0 + return sum( + 1 + for item in results + if isinstance(item, dict) and item.get("issue_severity") == "HIGH" + ) + + def public_domains() -> list[str]: excluded = {"auth", "core", "testing"} return sorted({row.sdk_package for row in parse_inventory() if row.sdk_package not in excluded}) @@ -95,6 +202,8 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: spec_report = read_json(args.spec_report) reference_report = read_json(args.reference_report) docstring_report = read_json(args.docstring_report) + changelog_report = read_json(args.changelog_report) + bandit_report = read_json(args.bandit_report) tutorials = markdown_files("tutorials") how_to = markdown_files("how-to") @@ -104,6 +213,7 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: placeholders = placeholder_count() docstring_gaps = report_value(docstring_report, "gap_count") + changelog_gaps = report_value(changelog_report, "gap_count") reference_gaps = report_value(reference_report, "gap_count") inventory_gaps = report_value(inventory_report, "gap_count") spec_gaps = report_value(spec_report, "gap_count") @@ -127,10 +237,21 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: example_grade = 1.0 if harness_enabled else 0.0 explanation_target = 10 explanation_grade = 1.0 if len(explanations) >= explanation_target else 0.25 + rename_gate_enabled = pr_template_has_public_rename_gate() + debug_info_safe = debug_info_contract_is_documented() + testing_documented = testing_contract_is_documented() + serialization_documented = serialization_contract_is_documented() + context_manager_documented = context_manager_contract_is_documented() + version = sdk_version() + semver_ok = semver_is_valid(version) and "Semantic Versioning" in read_text(ROOT / "CHANGELOG.md") + deprecation_warning_tested = deprecation_warning_contract_is_tested() + bandit_high = bandit_high_count(bandit_report) + ttfc = ttfc_minutes(args) + ttfc_ok = ttfc is not None and ttfc <= 15.0 return { "generated_at": datetime.now(UTC).isoformat(), - "sdk_version": sdk_version(), + "sdk_version": version, "diataxis_matrix": { "tutorials": tutorials, "how-to": how_to, @@ -142,7 +263,12 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: "public_contract_coverage": public_contract_coverage, "disabled_criteria": ["12"], "subcriteria": { - "15.1": grade(0.5, "getting-started.md существует; TTFC ещё не измерен"), + "15.1": grade( + 1.0 if ttfc_ok else 0.5, + f"TTFC={ttfc:.2f} минут, tutorial проходит цель <=15 минут" + if ttfc_ok + else "getting-started.md существует; TTFC ещё не измерен", + ), "15.2": grade( domain_grade, f"покрыто {len(domain_coverage)} из {len(domains)} публичных доменов", @@ -155,7 +281,10 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: explanation_grade, f"explanations pages={len(explanations)} из {explanation_target}", ), - "15.5": grade(0.5, "CHANGELOG подключён в docs/site/changelog.md"), + "15.5": grade( + 1.0 if changelog_gaps == 0 else 0.5, + f"CHANGELOG подключён; changelog sections gaps={changelog_gaps}", + ), "15.6": grade( example_grade, "pytest tests/docs/ включён в docs-strict" @@ -164,25 +293,65 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: ), }, "supporting_gates": { - "7.3_debug_info_safe_by_default": grade(0.5, "debug_info есть в client.md"), - "7.5_bandit_high_severity": grade(0.0, "bandit gate ещё не подключён"), + "7.3_debug_info_safe_by_default": grade( + 1.0 if debug_info_safe else 0.5, + "debug_info документирован и покрыт тестом на отсутствие секретов" + if debug_info_safe + else "debug_info есть в client.md", + ), + "7.5_bandit_high_severity": grade( + 1.0 if bandit_report and bandit_high == 0 else 0.0, + f"bandit high severity findings={bandit_high}" + if bandit_report + else "bandit gate ещё не подключён", + ), "16.1_fake_transport_namespace": grade(1.0, "avito.testing экспортирует FakeTransport"), - "16.2_mock_contract_documented": grade(0.5, "reference/testing.md создан"), - "16.3_json_serializable_models": grade(0.5, "reference/models.md создан"), - "16.4_context_manager_close": grade(0.5, "reference/client.md создан"), - "18.1_semver_compliant": grade(0.5, "version читается из pyproject.toml"), + "16.2_mock_contract_documented": grade( + 1.0 if testing_documented else 0.5, + "FakeTransport/as_client/RecordedRequest задокументированы и покрыты тестом" + if testing_documented + else "reference/testing.md создан", + ), + "16.3_json_serializable_models": grade( + 1.0 if serialization_documented else 0.5, + "to_dict/model_dump документированы и покрыты JSON-serialization тестом" + if serialization_documented + else "reference/models.md создан", + ), + "16.4_context_manager_close": grade( + 1.0 if context_manager_documented else 0.5, + "context manager/close/closed-client behavior документированы и покрыты тестом" + if context_manager_documented + else "reference/client.md создан", + ), + "18.1_semver_compliant": grade( + 1.0 if semver_ok else 0.5, + f"version {version} соответствует SemVer, CHANGELOG фиксирует Semantic Versioning" + if semver_ok + else "version читается из pyproject.toml", + ), "18.2_deprecation_period_2minor": grade( 1.0 if inventory_gaps == 0 else 0.0, f"inventory coverage gaps={inventory_gaps}", ), "18.3_deprecation_warning_emitted": grade( - 0.75, - "tests/contracts/test_deprecation_warnings.py покрывает inventory deprecated", + 1.0 if deprecation_warning_tested else 0.75, + "deprecated inventory symbols покрыты тестом DeprecationWarning и CHANGELOG" + if deprecation_warning_tested + else "tests/contracts/test_deprecation_warnings.py покрывает inventory deprecated", + ), + "18.4_changelog_sections": grade( + 1.0 if changelog_gaps == 0 else 0.0, + f"changelog sections gaps={changelog_gaps}", + ), + "18.5_public_renames_via_alias": grade( + 1.0 if rename_gate_enabled else 0.0, + "PR template содержит gate публичного переименования" + if rename_gate_enabled + else "PR template gate ещё не добавлен", ), - "18.4_changelog_sections": grade(0.0, "check_changelog_sections.py ещё не добавлен"), - "18.5_public_renames_via_alias": grade(0.0, "PR template gate ещё не добавлен"), }, - "ttfc_minutes": None, + "ttfc_minutes": ttfc, "lychee_broken_links": 0, "placeholder_count": placeholders, "inventory_coverage_gaps": inventory_gaps, @@ -190,7 +359,8 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: "reference_public_gaps": reference_gaps, "docstring_contract_gaps": docstring_gaps, "reference_explanation_examples_gaps": 0, - "changelog_sections_gaps": 0, + "changelog_sections_gaps": changelog_gaps, + "bandit_high_severity_gaps": bandit_high, } @@ -203,6 +373,11 @@ def main() -> None: parser.add_argument( "--docstring-report", type=Path, default=ROOT / "docstring-contract-report.json" ) + parser.add_argument( + "--changelog-report", type=Path, default=ROOT / "changelog-sections-report.json" + ) + parser.add_argument("--bandit-report", type=Path, default=ROOT / "bandit-report.json") + parser.add_argument("--ttfc-minutes", type=float, default=None) args = parser.parse_args() report = build_report(args) diff --git a/scripts/check_changelog_sections.py b/scripts/check_changelog_sections.py new file mode 100644 index 0000000..2d791d4 --- /dev/null +++ b/scripts/check_changelog_sections.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import argparse +import json +import re +from dataclasses import asdict, dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_CHANGELOG = ROOT / "CHANGELOG.md" +DEFAULT_OUTPUT = ROOT / "changelog-sections-report.json" +REQUIRED_SECTIONS = ("Added", "Changed", "Deprecated", "Removed", "Fixed") + + +@dataclass(slots=True, frozen=True) +class ChangelogGap: + version: str + section: str + reason: str + + +def current_release_block(text: str) -> tuple[str, str]: + heading = re.search(r"^## \[(?P[^\]]+)\].*$", text, re.MULTILINE) + if heading is None: + raise ValueError("В CHANGELOG.md не найден заголовок версии `## [...]`.") + next_heading = re.search(r"^## \[", text[heading.end() :], re.MULTILINE) + end = heading.end() + next_heading.start() if next_heading is not None else len(text) + return heading.group("version"), text[heading.end() : end] + + +def collect_gaps(path: Path) -> list[ChangelogGap]: + version, block = current_release_block(path.read_text(encoding="utf-8")) + sections = set(re.findall(r"^### ([A-Za-z]+)\s*$", block, re.MULTILINE)) + return [ + ChangelogGap(version, section, "секция отсутствует в текущем релизном блоке") + for section in REQUIRED_SECTIONS + if section not in sections + ] + + +def write_report(gaps: list[ChangelogGap], output: Path) -> None: + report = { + "required_sections": list(REQUIRED_SECTIONS), + "gaps": [asdict(gap) for gap in gaps], + "gap_count": len(gaps), + } + output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Проверить секции текущего CHANGELOG-блока.") + parser.add_argument("--changelog", type=Path, default=DEFAULT_CHANGELOG) + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--strict", action="store_true") + args = parser.parse_args() + + gaps = collect_gaps(args.changelog) + write_report(gaps, args.output) + if args.strict and gaps: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_inventory_coverage.py b/scripts/check_inventory_coverage.py index 8b4853c..655423a 100644 --- a/scripts/check_inventory_coverage.py +++ b/scripts/check_inventory_coverage.py @@ -1,13 +1,12 @@ from __future__ import annotations import argparse -import importlib -import inspect import json from dataclasses import asdict, dataclass from pathlib import Path from parse_inventory import InventoryRow, parse_inventory +from public_sdk_surface import resolve_public_method ROOT = Path(__file__).resolve().parents[1] DEFAULT_OUTPUT = ROOT / "inventory-coverage-report.json" @@ -38,25 +37,15 @@ def removal_is_two_minor_later(deprecated_since: str, removal_version: str) -> b def domain_has_public_method(row: InventoryRow) -> bool: - if row.domain_object == "AvitoClient.auth()": - from avito import AvitoClient - - return hasattr(AvitoClient, "auth") - - try: - module = importlib.import_module(f"avito.{row.sdk_package}") - except ModuleNotFoundError: - return False - - domain_class = getattr(module, row.domain_object, None) - if domain_class is None: - return False - return inspect.isclass(domain_class) and hasattr(domain_class, row.sdk_public_method) + return resolve_public_method(row) is not None def collect_gaps(rows: list[InventoryRow]) -> list[InventoryGap]: gaps: list[InventoryGap] = [] for row in rows: + if not domain_has_public_method(row): + gaps.append(gap(row, "не найден публичный SDK-символ")) + if row.deprecated: missing = [ name @@ -75,9 +64,6 @@ def collect_gaps(rows: list[InventoryRow]) -> list[InventoryGap]: description_marks_deprecated = "deprecated" in row.description.lower() if description_marks_deprecated and not row.deprecated: gaps.append(gap(row, "описание содержит deprecated, но deprecated=нет")) - if row.deprecated and not domain_has_public_method(row): - gaps.append(gap(row, "не найден публичный SDK-символ")) - return gaps diff --git a/scripts/check_public_docstrings.py b/scripts/check_public_docstrings.py index 3b15c7a..009f01f 100644 --- a/scripts/check_public_docstrings.py +++ b/scripts/check_public_docstrings.py @@ -1,17 +1,18 @@ from __future__ import annotations import argparse -import importlib import inspect import json from dataclasses import asdict, dataclass from pathlib import Path from parse_inventory import InventoryRow, parse_inventory +from public_sdk_surface import resolve_public_method ROOT = Path(__file__).resolve().parents[1] DEFAULT_OUTPUT = ROOT / "docstring-contract-report.json" EXCEPTION_METADATA_FIELDS = ("operation", "status", "request_id", "attempt", "method", "endpoint") +OVERRIDE_PARAMS = ("timeout", "retries", "dry_run", "idempotency_key", "page_size") @dataclass(slots=True, frozen=True) @@ -21,72 +22,116 @@ class DocstringGap: reason: str -def domain_method(row: InventoryRow) -> object | None: - if row.domain_object == "AvitoClient.auth()": - from avito import AvitoClient +def inventory_symbol_name(row: InventoryRow) -> str: + return f"avito.{row.sdk_package}.{row.domain_object}.{row.sdk_public_method}" - return getattr(AvitoClient, "auth", None) +def public_parameters(method: object) -> set[str]: try: - module = importlib.import_module(f"avito.{row.sdk_package}") - except ModuleNotFoundError: - return None - domain_class = getattr(module, row.domain_object, None) - if domain_class is None: - return None - return getattr(domain_class, row.sdk_public_method, None) + return set(inspect.signature(method).parameters) + except (TypeError, ValueError): + return set() -def symbol_name(row: InventoryRow) -> str: - return f"avito.{row.sdk_package}.{row.domain_object}.{row.sdk_public_method}" +def has_return_annotation(method: object) -> bool: + try: + return inspect.signature(method).return_annotation is not inspect.Signature.empty + except (TypeError, ValueError): + return False + + +def needs_empty_behavior_note(method_name: str, doc: str, method: object) -> bool: + if any(marker in doc for marker in ("none", "null", "пуст", "empty")): + return False + if method_name.startswith("list") or method_name in {"get_items", "get_by_ids"}: + return True + try: + annotation = inspect.signature(method).return_annotation + except (TypeError, ValueError): + return False + return "PaginatedList" in str(annotation) or "list[" in str(annotation) + + +def doc_mentions_all(doc: str, markers: set[str]) -> bool: + return all(marker.lower() in doc for marker in markers) def collect_gaps(rows: list[InventoryRow]) -> list[DocstringGap]: gaps: list[DocstringGap] = [] seen: set[str] = set() for row in rows: - symbol = symbol_name(row) + resolved = resolve_public_method(row) + symbol = resolved.symbol if resolved is not None else inventory_symbol_name(row) if symbol in seen: continue seen.add(symbol) - method = domain_method(row) - if method is None: + if resolved is None: gaps.append(DocstringGap(symbol, "exists", "публичный метод не найден")) continue - doc = inspect.getdoc(method) or "" + doc = inspect.getdoc(resolved.method) or "" lowered = doc.lower() if not doc: gaps.append(DocstringGap(symbol, "docstring", "docstring отсутствует")) continue - expected = { - "return_model": ("возвращ", "return", row.response_type.lower()), - "nullable_empty": ("none", "null", "пуст", "empty"), - "overrides": ("timeout", "retries", "dry_run", "idempotency_key", "page_size"), - "idempotency": ("идемпот", "idempot"), - "raises": ("raises", "исключ", "ошиб", *EXCEPTION_METADATA_FIELDS), - } - if row.request_type != "NoRequest": - expected["dry_run"] = ("dry_run", "транспорт", "transport") - - for aspect, markers in expected.items(): - if not any(marker.lower() in lowered for marker in markers): - gaps.append( - DocstringGap( - symbol, - aspect, - "docstring не описывает обязательный contract-аспект", - ) - ) + params = public_parameters(resolved.method) + override_params = params.intersection(OVERRIDE_PARAMS) + + if not has_return_annotation(resolved.method) and not any( + marker in lowered for marker in ("возвращ", "return", row.response_type.lower()) + ): + gaps.append(gap(symbol, "return_model")) + + if needs_empty_behavior_note(resolved.method_name, lowered, resolved.method): + gaps.append(gap(symbol, "nullable_empty")) + + if override_params and not doc_mentions_all(lowered, override_params): + gaps.append(gap(symbol, "overrides")) + + if "idempotency_key" in params and not any( + marker in lowered for marker in ("идемпот", "idempot", "idempotency_key") + ): + gaps.append(gap(symbol, "idempotency")) + + if not any( + marker in lowered + for marker in ("raises", "исключ", "ошиб", *EXCEPTION_METADATA_FIELDS) + ): + gaps.append(gap(symbol, "raises")) + + if "dry_run" in params and not any( + marker in lowered for marker in ("dry_run", "транспорт", "transport") + ): + gaps.append(gap(symbol, "dry_run")) return gaps +def gap(symbol: str, aspect: str) -> DocstringGap: + return DocstringGap(symbol, aspect, "docstring не описывает обязательный contract-аспект") + + def write_report(rows: list[InventoryRow], gaps: list[DocstringGap], output: Path) -> None: + by_aspect: dict[str, int] = {} + by_domain: dict[str, int] = {} + for item in gaps: + by_aspect[item.aspect] = by_aspect.get(item.aspect, 0) + 1 + parts = item.symbol.split(".") + domain = parts[1] if len(parts) > 1 else "unknown" + by_domain[domain] = by_domain.get(domain, 0) + 1 + report = { - "checked_symbols": len({symbol_name(row) for row in rows}), + "checked_symbols": len( + { + resolved.symbol if (resolved := resolve_public_method(row)) is not None + else inventory_symbol_name(row) + for row in rows + } + ), "required_exception_metadata_fields": list(EXCEPTION_METADATA_FIELDS), + "by_aspect": dict(sorted(by_aspect.items())), + "by_domain": dict(sorted(by_domain.items())), "gaps": [asdict(gap) for gap in gaps], "gap_count": len(gaps), } diff --git a/scripts/public_sdk_surface.py b/scripts/public_sdk_surface.py new file mode 100644 index 0000000..f8f60f8 --- /dev/null +++ b/scripts/public_sdk_surface.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +import importlib +import inspect +from collections.abc import Callable +from dataclasses import dataclass + +try: + from parse_inventory import InventoryRow +except ModuleNotFoundError: + from scripts.parse_inventory import InventoryRow + + +@dataclass(slots=True, frozen=True) +class PublicMethod: + sdk_package: str + domain_object: str + method_name: str + method: object + + @property + def symbol(self) -> str: + return f"avito.{self.sdk_package}.{self.domain_object}.{self.method_name}" + + +MethodAlias = Callable[[InventoryRow], str | None] + + +EXPLICIT_METHOD_ALIASES: dict[tuple[str, str, str], str] = { + ("accounts", "Account", "get_user_info_self"): "get_self", + ("accounts", "Account", "get_user_balance"): "get_balance", + ("accounts", "AccountHierarchy", "get_check_ah_user_v1"): "get_status", + ("accounts", "AccountHierarchy", "list_employees_v1"): "list_employees", + ("accounts", "AccountHierarchy", "create_link_items_v1"): "link_items", + ("accounts", "AccountHierarchy", "list_company_phones_v1"): "list_company_phones", + ("accounts", "AccountHierarchy", "list_items_by_employee_id_v1"): "list_items_by_employee", + ("ads", "Ad", "get_item_info"): "get", + ("ads", "Ad", "get_items_info"): "list", + ("ads", "Ad", "update_update_price"): "update_price", + ("ads", "AdPromotion", "update_item_vas"): "apply_vas_direct", + ("ads", "AdPromotion", "update_item_vas_package_v2"): "apply_vas_package", + ("ads", "AdPromotion", "update_apply_vas"): "apply_vas", + ("ads", "AdStats", "get_item_stats_shallow"): "get_item_stats", + ("ads", "AutoloadProfile", "create_upload"): "upload_by_url", + ("ads", "AutoloadProfile", "get_user_docs_node_fields"): "get_node_fields", + ("ads", "AutoloadProfile", "get_user_docs_tree"): "get_tree", + ("ads", "AutoloadProfile", "get_profile_v2"): "get", + ("ads", "AutoloadProfile", "create_or_update_profile_v2"): "save", + ("ads", "AutoloadReport", "list_reports_v2"): "list", + ("ads", "AutoloadReport", "get_autoload_items_info_v2"): "get_items_info", + ("ads", "AutoloadReport", "get_report_items_by_id"): "get_items", + ("ads", "AutoloadReport", "get_report_items_fees_by_id"): "get_fees", + ("ads", "AutoloadReport", "get_last_completed_report_v3"): "get_last_completed", + ("ads", "AutoloadReport", "get_report_by_id_v3"): "get", + ("autoteka", "AutotekaVehicle", "get_catalogs_resolve"): "resolve_catalog", + ("autoteka", "AutotekaMonitoring", "list_monitoring_bucket_delete"): "remove_bucket", + ("autoteka", "AutotekaMonitoring", "delete_monitoring_bucket_remove"): "delete_bucket", + ( + "autoteka", + "AutotekaMonitoring", + "get_monitoring_get_reg_actions", + ): "get_monitoring_reg_actions", + ("autoteka", "AutotekaReport", "list_report_list"): "list_reports", + ("autoteka", "AutotekaScoring", "get_scoring_get_by_id"): "get_scoring_by_id", + ("autoteka", "AutotekaVehicle", "get_specification_get_by_id"): "get_specification_by_id", + ( + "autoteka", + "AutotekaReport", + "create_sync_create_report_by_reg_number", + ): "create_sync_report_by_reg_number", + ( + "autoteka", + "AutotekaReport", + "create_sync_create_report_by_vin", + ): "create_sync_report_by_vin", + ("cpa", "CpaChat", "get_chat_by_action_id"): "get", + ("cpa", "CpaCall", "create_create_complaint"): "create_complaint", + ("cpa", "CpaCall", "create_calls_by_time_v2"): "list", + ("cpa", "CpaChat", "create_chats_by_time"): "list", + ("cpa", "CpaLead", "create_balance_info_v3"): "get_balance_info", + ("cpa", "CallTrackingCall", "create_call_by_id"): "get", + ("cpa", "CallTrackingCall", "create_calls"): "list", + ("cpa", "CallTrackingCall", "get_record_by_call_id"): "download", + ("jobs", "Application", "get_applications_apply_actions"): "apply", + ("jobs", "Application", "list_applications_get_by_ids"): "list", + ("jobs", "Application", "list_applications_get_ids"): "list", + ("jobs", "Application", "list_applications_get_states"): "get_states", + ("jobs", "Application", "get_applications_set_is_viewed"): "update", + ("jobs", "JobWebhook", "delete_applications_webhook_delete"): "delete", + ("jobs", "JobWebhook", "get_applications_webhook_get"): "get", + ("jobs", "JobWebhook", "update_applications_webhook_put"): "update", + ("jobs", "JobWebhook", "list_applications_webhooks_get"): "list", + ("jobs", "Resume", "list_resumes_get"): "list", + ("jobs", "Resume", "get_resume_get_contacts"): "get_contacts", + ("jobs", "Resume", "get_resume_get_item"): "get", + ("jobs", "Vacancy", "create_vacancy_create"): "create", + ("jobs", "Vacancy", "delete_vacancy_archive"): "delete", + ("jobs", "Vacancy", "update_vacancy_update"): "update", + ("jobs", "Vacancy", "create_vacancy_prolongate"): "prolongate", + ("jobs", "Vacancy", "list_search_vacancy"): "list", + ("jobs", "Vacancy", "create_vacancy_create_v2"): "create", + ("jobs", "Vacancy", "get_vacancies_get_by_ids"): "get_by_ids", + ("jobs", "Vacancy", "get_vacancy_get_statuses"): "get_statuses", + ("jobs", "Vacancy", "update_vacancy_update_v2"): "update", + ("jobs", "Vacancy", "get_vacancy_get_item"): "get", + ("jobs", "Vacancy", "update_vacancy_auto_renewal"): "update_auto_renewal", + ("jobs", "JobDictionary", "list_dicts"): "list", + ("jobs", "JobDictionary", "list_dict_by_id"): "get", + ("messenger", "ChatMessage", "create_send_message"): "send_message", + ("messenger", "ChatMessage", "create_send_image_message"): "send_image", + ("messenger", "ChatMessage", "delete_message"): "delete", + ("messenger", "Chat", "create_chat_read"): "mark_read", + ("messenger", "ChatMedia", "create_upload_images"): "upload_images", + ("messenger", "ChatWebhook", "get_subscriptions"): "list", + ("messenger", "ChatWebhook", "delete_webhook_unsubscribe"): "unsubscribe", + ("messenger", "Chat", "create_blacklist_v2"): "blacklist", + ("messenger", "Chat", "get_chats_v2"): "list", + ("messenger", "Chat", "get_chat_by_id_v2"): "get", + ("messenger", "ChatMessage", "list_messages_v3"): "list", + ("messenger", "ChatWebhook", "update_webhook_v3"): "subscribe", + ("messenger", "SpecialOfferCampaign", "create_multi_confirm"): "confirm_multi", + ("messenger", "SpecialOfferCampaign", "create_multi_create"): "create_multi", + ("orders", "DeliveryOrder", "delete_cancel_announcement3_pl"): "delete", + ("orders", "DeliveryOrder", "create_announcement3_pl"): "create_announcement", + ("orders", "DeliveryOrder", "create_parcel"): "create", + ("orders", "DeliveryTask", "get_task"): "get", + ("orders", "Order", "create_accept_return_order"): "accept_return_order", + ("orders", "Order", "get_apply_transition"): "apply", + ("orders", "Order", "create_check_confirmation_code"): "check_confirmation_code", + ("orders", "Order", "create_cnc_set_details"): "set_cnc_details", + ("orders", "Order", "get_set_courier_delivery_range"): "set_courier_delivery_range", + ("orders", "Order", "update_set_order_tracking_number"): "update_tracking_number", + ("orders", "Order", "get_orders"): "list", + ("orders", "OrderLabel", "create_generate_labels"): "create", + ("orders", "OrderLabel", "create_generate_labels_extended"): "create", + ("orders", "OrderLabel", "get_download_label"): "download", + ("orders", "Stock", "get_получение_остатков"): "get", + ("orders", "Stock", "update_редактирование_остатков"): "update", + ("promotion", "TrxPromotion", "create_trx_promo_open_api_apply"): "apply", + ("promotion", "TrxPromotion", "delete_trx_promo_open_api_cancel"): "delete", + ("promotion", "TrxPromotion", "get_trx_promo_open_api_commissions"): "get_commissions", + ("promotion", "AutostrategyCampaign", "create_autostrategy_budget"): "create_budget", + ("promotion", "AutostrategyCampaign", "create_autostrategy_campaign"): "create", + ( + "promotion", + "AutostrategyCampaign", + "update_edit_autostrategy_campaign", + ): "update", + ( + "promotion", + "AutostrategyCampaign", + "get_autostrategy_campaign_info", + ): "get", + ( + "promotion", + "AutostrategyCampaign", + "delete_stop_autostrategy_campaign", + ): "delete", + ("promotion", "AutostrategyCampaign", "list_autostrategy_campaigns"): "list", + ("promotion", "AutostrategyCampaign", "get_autostrategy_stat"): "get_stat", + ("promotion", "TargetActionPricing", "delete_promotion"): "delete", + ("promotion", "TargetActionPricing", "update_auto_bid"): "update_auto", + ("promotion", "TargetActionPricing", "update_manual_bid"): "update_manual", + ("promotion", "BbipPromotion", "create_bbip_forecasts_by_items_v1"): "get_forecasts", + ("promotion", "BbipPromotion", "update_bbip_order_for_items_v1"): "create_order", + ("promotion", "BbipPromotion", "create_bbip_suggests_by_items_v1"): "get_suggests", + ("promotion", "PromotionOrder", "create_dict_of_services_v1"): "get_service_dictionary", + ("promotion", "PromotionOrder", "list_services_by_items_v1"): "list_services", + ("promotion", "PromotionOrder", "list_orders_by_user_v1"): "list_orders", + ("promotion", "PromotionOrder", "get_order_status_v1"): "get_order_status", + ( + "realty", + "RealtyAnalyticsReport", + "get_market_price_correspondence_v1", + ): "get_market_price_correspondence", + ("ratings", "ReviewAnswer", "create_review_answer_v1"): "create", + ("ratings", "ReviewAnswer", "delete_review_answer_v1"): "delete", + ("ratings", "RatingProfile", "get_ratings_info_v1"): "get", + ("ratings", "Review", "list_reviews_v1"): "list", +} + +SANDBOX_DELIVERY_ALIASES: dict[str, str] = { + "create_track_announcement": "track_announcement", + "delete_cancel_parcel": "cancel_parcel", + "get_check_confirmation_code": "check_confirmation_code", + "create_set_order_properties": "set_order_properties", + "create_set_order_real_address": "set_order_real_address", + "create_tracking": "tracking", + "delete_prohibit_order_acceptance": "prohibit_order_acceptance", + "create_add_sorting_center": "add_sorting_center", + "create_add_areas_sandbox": "add_areas", + "update_add_tags_to_sorting_center": "add_tags_to_sorting_center", + "create_add_terminals_sandbox": "add_terminals", + "update_update_terms": "update_terms", + "create_add_tariff_sandbox_v2": "add_tariff", + "create_v1cancel_announcement": "cancel_sandbox_announcement", + "delete_v1_cancel_parcel": "cancel_sandbox_parcel", + "create_v1change_parcel": "change_sandbox_parcel", + "create_v1create_announcement": "create_sandbox_announcement", + "get_v1get_announcement_event": "get_sandbox_announcement_event", + "get_v1get_change_parcel_info": "get_sandbox_change_parcel_info", + "get_v1get_parcel_info": "get_sandbox_parcel_info", + "get_v1get_registered_parcel_id": "get_sandbox_registered_parcel_id", + "create_sandbox_parcel_v2": "create_parcel", +} + + +def resolve_public_method(row: InventoryRow) -> PublicMethod | None: + if row.domain_object == "AvitoClient.auth()": + from avito import AvitoClient + + method = getattr(AvitoClient, "auth", None) + if method is None: + return None + return PublicMethod("client", "AvitoClient", "auth", method) + + try: + module = importlib.import_module(f"avito.{row.sdk_package}") + except ModuleNotFoundError: + return None + + domain_class = getattr(module, row.domain_object, None) + if domain_class is None or not inspect.isclass(domain_class): + return None + + method_name = public_method_name(row) + method = getattr(domain_class, method_name, None) + if method is None: + return None + return PublicMethod(row.sdk_package, row.domain_object, method_name, method) + + +def public_method_name(row: InventoryRow) -> str: + explicit = EXPLICIT_METHOD_ALIASES.get( + (row.sdk_package, row.domain_object, row.sdk_public_method) + ) + if explicit is not None: + return explicit + if row.sdk_package == "orders" and row.domain_object == "SandboxDelivery": + return SANDBOX_DELIVERY_ALIASES.get(row.sdk_public_method, row.sdk_public_method) + return row.sdk_public_method From 4133e627ada124e363f046417f5b555c750a7338 Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Fri, 24 Apr 2026 02:06:21 +0300 Subject: [PATCH 8/9] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B0?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 +- docs/site/how-to/.pages | 6 + docs/site/how-to/diagnostics-and-logging.md | 106 +++++++++++++++ docs/site/how-to/idempotency.md | 70 ++++++++++ docs/site/how-to/index.md | 8 +- docs/site/how-to/pagination.md | 90 +++++++++++++ docs/site/how-to/per-operation-overrides.md | 91 +++++++++++++ docs/site/how-to/security-practices.md | 91 +++++++++++++ .../how-to/testing-with-fake-transport.md | 124 ++++++++++++++++++ 9 files changed, 588 insertions(+), 3 deletions(-) create mode 100644 docs/site/how-to/diagnostics-and-logging.md create mode 100644 docs/site/how-to/idempotency.md create mode 100644 docs/site/how-to/pagination.md create mode 100644 docs/site/how-to/per-operation-overrides.md create mode 100644 docs/site/how-to/security-practices.md create mode 100644 docs/site/how-to/testing-with-fake-transport.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 60e6c00..315c4e5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -29,7 +29,10 @@ "Bash(poetry install *)", "Bash(poetry lock *)", "Bash(awk -F'|' '{print $4}')", - "Bash(grep -v \"^$\")" + "Bash(grep -v \"^$\")", + "Bash(grep -n \"^\\\\`\\\\`\\\\`\" /Users/n.baryshnikov/Projects/avito_python_api/docs/site/how-to/idempotency.md)", + "Bash(grep -n \"^\\\\`\\\\`\\\\`\" /Users/n.baryshnikov/Projects/avito_python_api/docs/site/how-to/per-operation-overrides.md)", + "Bash(grep -n \"^\\\\`\\\\`\\\\`\" /Users/n.baryshnikov/Projects/avito_python_api/docs/site/how-to/security-practices.md)" ] } } diff --git a/docs/site/how-to/.pages b/docs/site/how-to/.pages index 7eab4f7..74c0354 100644 --- a/docs/site/how-to/.pages +++ b/docs/site/how-to/.pages @@ -11,3 +11,9 @@ nav: - realty-booking.md - cpa-calltracking.md - ratings-and-tariffs.md + - pagination.md + - per-operation-overrides.md + - idempotency.md + - testing-with-fake-transport.md + - diagnostics-and-logging.md + - security-practices.md diff --git a/docs/site/how-to/diagnostics-and-logging.md b/docs/site/how-to/diagnostics-and-logging.md new file mode 100644 index 0000000..e9f073c --- /dev/null +++ b/docs/site/how-to/diagnostics-and-logging.md @@ -0,0 +1,106 @@ +# Диагностика и логирование + +SDK предоставляет безопасный диагностический снимок через `debug_info()` и типизированные исключения с полной информацией об операции. Это упрощает отладку без риска утечки секретов в логи. + +## Диагностический снимок + +`debug_info()` возвращает `TransportDebugInfo` — снимок transport-настроек без OAuth-секретов. Его безопасно печатать в логи и передавать в отчёты об ошибках. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + info = avito.debug_info() + +print(info.base_url) +print(info.retry_max_attempts) +print(info.timeout_read) +print(info.requires_auth) +``` + +## Поля исключений + +Каждое исключение SDK содержит поля для диагностики: + +- `operation` — имя SDK-операции (например, `ads.get_item`); +- `status_code` — HTTP-статус ответа; +- `error_code` — код ошибки из тела ответа API; +- `message` — читаемое описание ошибки; +- `metadata` — дополнительные поля с редактированными секретами. + +```text +from avito import AvitoClient +from avito.core.exceptions import AvitoError + +with AvitoClient.from_env() as avito: + try: + item = avito.ad(item_id=999, user_id=7).get() + except AvitoError as exc: + print(exc.operation) + print(exc.status_code) + print(exc.error_code) + print(str(exc)) +``` + +## Обработка специфичных ошибок + +SDK предоставляет иерархию исключений. Ловите конкретные типы для разной обработки: + +```text +from avito.core.exceptions import ( + AuthenticationError, + AuthorizationError, + RateLimitError, + NotFoundError, + AvitoError, +) + +try: + result = avito.account().get_self() +except AuthenticationError: + # 401 — протух токен, нужно обновить credentials + ... +except RateLimitError as exc: + # 429 — слишком частые запросы + print(exc.status_code) +except AuthorizationError: + # 403 — нет доступа к ресурсу + ... +except AvitoError as exc: + # любая другая ошибка SDK + print(str(exc)) +``` + +## Безопасное логирование + +`debug_info()` — единственный публичный способ получить диагностику без секретов. Не логируйте `to_dict()` / `model_dump()` моделей, которые могут содержать чувствительные данные пользователя. + +```python +from avito import AvitoClient + +import logging +logger = logging.getLogger(__name__) + +with AvitoClient.from_env() as avito: + info = avito.debug_info() + logger.info("SDK инициализирован: base_url=%s, retry=%s", info.base_url, info.retry_max_attempts) +``` + +## После close() + +После `close()` или выхода из контекстного менеджера любой SDK-вызов поднимает `ConfigurationError`. Проверяйте это в долгоживущих сервисах. + +```python +from avito import AvitoClient +from avito.core.exceptions import ConfigurationError + +avito = AvitoClient.from_env() +avito.close() + +try: + avito.account().get_self() +except ConfigurationError as exc: + print(str(exc)) +``` + +Полный контракт исключений описан в [reference по исключениям](../reference/exceptions.md). Security-модель SDK разобрана в [explanations](../explanations/security-and-redaction.md). diff --git a/docs/site/how-to/idempotency.md b/docs/site/how-to/idempotency.md new file mode 100644 index 0000000..537cc18 --- /dev/null +++ b/docs/site/how-to/idempotency.md @@ -0,0 +1,70 @@ +# Идемпотентность + +Write-операции в SDK могут завершиться успешно на стороне API, но не вернуть ответ из-за сетевой ошибки. Параметр `idempotency_key` позволяет безопасно повторить такой вызов: API обработает повторный запрос с тем же ключом как no-op и вернёт оригинальный результат. + +## Когда нужен idempotency_key + +Используйте ключ для любой write-операции, которую вы можете повторить при сбое. Методы, принимающие `idempotency_key`, явно документируют его в docstring. Без ключа повтор может создать дублирующую операцию. + +## Обновление цены + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + result = avito.ad(item_id=101, user_id=7).update_price( + price=10900, + idempotency_key="price-update-user7-item101-v1", + ) + +print(result.item_id) +print(result.price) +``` + +При сетевом сбое передайте тот же `idempotency_key` повторно. Ключ должен однозначно идентифицировать логическую операцию: включайте идентификаторы ресурса и версию намерения. + +## Пометка чата как прочитанного + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + result = avito.chat(chat_id="chat-1", user_id=7).mark_read( + idempotency_key="mark-read-user7-chat1-2026-04-24", + ) + +print(result.status) +``` + +## Правила формирования ключа + +Хороший ключ идемпотентности: + +- уникален для каждой логической операции (разные ресурсы → разные ключи); +- воспроизводим при повторе (тот же ключ при той же попытке); +- не переиспользуется для разных намерений (изменение цены с 9900 → 10900 и с 10900 → 12000 должны иметь разные ключи). + +```text +price-update-{user_id}-{item_id}-{target_price} +mark-read-{user_id}-{chat_id}-{date} +``` + +## Dry-run и idempotency_key + +При `dry_run=True` `idempotency_key` не принимается, потому что transport-вызов не выполняется. При переходе от dry-run к реальному вызову добавляйте ключ явно. + +```text +# dry-run — проверяем payload, ключ не нужен +preview = avito.ad_promotion(item_id=101, user_id=7).apply_vas( + codes=["xl"], + dry_run=True, +) + +# реальный вызов — добавляем ключ +result = avito.ad_promotion(item_id=101, user_id=7).apply_vas( + codes=["xl"], + idempotency_key="apply-vas-user7-item101-xl-2026-04-24", +) +``` + +Полный контракт overrides описан в [reference по конфигурации](../reference/config.md). Сценарий с dry-run разобран в [рецепте по продвижению](promotion-dry-run.md). diff --git a/docs/site/how-to/index.md b/docs/site/how-to/index.md index 5ed278f..2c835b4 100644 --- a/docs/site/how-to/index.md +++ b/docs/site/how-to/index.md @@ -9,13 +9,17 @@ How-to раздел собирает рецепты для конкретных | [Объявления, статистика и продвижение](ad-listing-and-stats.md) | Найти объявления, открыть карточку, прочитать статистику и подготовить VAS | | [Продвижение с dry-run](promotion-dry-run.md) | Проверить payload write-операции без сетевого вызова | | [Чаты, сообщения и изображения](chat-image-upload.md) | Отправить текст, загрузить изображение и отправить его в чат | -| Пагинация | Читать `PaginatedList` частями и материализовать весь список | | [Заказы, этикетки и остатки](order-labels.md) | Создать задачу генерации, скачать PDF и обновить остатки | | [Вакансии, отклики и резюме](job-applications.md) | Получить отклики, обработать их и настроить webhook | | [Отчёт Автотеки](autoteka-report.md) | Пройти цепочку VIN → preview → report | | [Бронирование недвижимости](realty-booking.md) | Обновить блокировки дат и цены | | [CPA и CallTracking](cpa-calltracking.md) | Найти звонки и скачать запись | | [Рейтинги, отзывы и тарифы](ratings-and-tariffs.md) | Получить отзывы, ответить на отзыв и прочитать тариф | -| Тестирование с FakeTransport | Проверить consumer-код без реального HTTP | +| [Пагинация](pagination.md) | Читать `PaginatedList` частями и материализовать весь список | +| [Per-operation overrides](per-operation-overrides.md) | Настроить таймауты, retry-политику и idempotency key | +| [Идемпотентность](idempotency.md) | Безопасно повторять write-операции с idempotency_key | +| [Тестирование с FakeTransport](testing-with-fake-transport.md) | Проверить consumer-код без реального HTTP | +| [Диагностика и логирование](diagnostics-and-logging.md) | Использовать `debug_info()` и типизированные исключения | +| [Безопасная работа с SDK](security-practices.md) | Хранить секреты и логировать безопасно | Для первого запуска начните с [быстрого старта](../tutorials/getting-started.md). Для поиска метода по операции API используйте [карту операций](../reference/operations.md). diff --git a/docs/site/how-to/pagination.md b/docs/site/how-to/pagination.md new file mode 100644 index 0000000..b23208f --- /dev/null +++ b/docs/site/how-to/pagination.md @@ -0,0 +1,90 @@ +# Пагинация + +`PaginatedList[T]` — ленивый контейнер: первая страница загружается при создании, остальные подгружаются только при обращении к данным за их пределами. Это позволяет работать с большими списками без полной загрузки всех страниц сразу. + +## Ленивая итерация + +`for`-цикл читает страницы по одной. Если вам нужны только первые несколько элементов, обход прекращается без лишних HTTP-запросов. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + listings = avito.ad(user_id=7).list(status="active", limit=2) + for item in listings: + print(item.item_id, item.title) +``` + +## Полная загрузка + +`materialize()` загружает все оставшиеся страницы и возвращает обычный `list[T]`. Используйте этот метод, когда нужны все элементы сразу. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + listings = avito.ad(user_id=7).list(status="active", limit=2) + items = listings.materialize() + +print(len(items)) +print(items[0].title) +``` + +## Доступ по индексу + +Обращение по индексу подгружает только нужные страницы. Отрицательный индекс вызывает полную загрузку. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + listings = avito.ad(user_id=7).list(status="active", limit=2) + first = listings[0] + +print(first.item_id) +``` + +## Контроль размера страницы + +Параметр `limit` управляет количеством элементов на странице. Меньший `limit` снижает объём первого ответа, больший — уменьшает число запросов при полной загрузке. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + compact = avito.ad(user_id=7).list(limit=1) + large = avito.ad(user_id=7).list(limit=10) + +print(compact[0].title) +print(large[0].title) +``` + +## Проверка общего числа элементов + +`len()` возвращает известный total из ответа API, если он был передан. Если total неизвестен, `len()` дозагружает все страницы. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + listings = avito.ad(user_id=7).list(status="active", limit=2) + total = len(listings) + +print(total) +``` + +## Пагинация со смещением + +Параметр `offset` позволяет начать чтение не с первого элемента. Используйте его для постраничного перехода с фиксированными позициями. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + page2 = avito.ad(user_id=7).list(limit=2, offset=0) + items = page2.materialize() + +print(items[0].item_id) +``` + +Полный контракт `PaginatedList` описан в [reference по пагинации](../reference/pagination.md). Семантика ленивой загрузки подробно разобрана в [explanations](../explanations/pagination-semantics.md). diff --git a/docs/site/how-to/per-operation-overrides.md b/docs/site/how-to/per-operation-overrides.md new file mode 100644 index 0000000..5f6c43b --- /dev/null +++ b/docs/site/how-to/per-operation-overrides.md @@ -0,0 +1,91 @@ +# Per-operation overrides + +SDK даёт два вида переопределения поведения: глобальные настройки `AvitoSettings` (таймауты, retry-политика) и параметры конкретных методов (`idempotency_key`, `limit` для пагинации). Глобальные настройки задаются один раз при создании клиента. + +## Таймауты + +`ApiTimeouts` управляет таймаутами connect, read, write и pool. Настраивается в `AvitoSettings` или через env-переменные. + +```python +from avito import AvitoSettings +from avito.core.types import ApiTimeouts + +settings = AvitoSettings( + timeouts=ApiTimeouts( + connect=3.0, + read=30.0, + write=20.0, + pool=5.0, + ) +) + +print(settings.timeouts.read) +``` + +Env-переменные: `AVITO_TIMEOUT_CONNECT`, `AVITO_TIMEOUT_READ`, `AVITO_TIMEOUT_WRITE`, `AVITO_TIMEOUT_POOL`. + +## Retry-политика + +`RetryPolicy` задаёт число попыток, backoff-фактор, максимальную задержку и список retryable HTTP-методов. + +```python +from avito import AvitoSettings +from avito.core.retries import RetryPolicy + +settings = AvitoSettings( + retry_policy=RetryPolicy( + max_attempts=5, + backoff_factor=1.0, + max_delay=60.0, + retry_on_rate_limit=True, + ) +) + +print(settings.retry_policy.max_attempts) +``` + +Env-переменные: `AVITO_RETRY_MAX_ATTEMPTS`, `AVITO_RETRY_BACKOFF_FACTOR`, `AVITO_RETRY_MAX_DELAY`. + +## Idempotency key для write-операций + +Write-операции с `dry_run=False` принимают `idempotency_key` для безопасного повтора при сетевых ошибках. Один и тот же ключ гарантирует, что повторный вызов не создаст дублирующую операцию на стороне API. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + result = avito.ad(item_id=101, user_id=7).update_price( + price=10900, + idempotency_key="price-update-101-2026-04-24", + ) + +print(result.item_id) +print(result.price) +``` + +Ключ должен быть уникальным для каждой логической операции. При повторе используйте тот же ключ. + +## Размер страницы для пагинации + +List-методы принимают `limit` для управления числом элементов на странице. Меньший `limit` снижает latency первого запроса, больший — уменьшает число запросов при `materialize()`. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + listings = avito.ad(user_id=7).list(limit=5) + items = listings.materialize() + +print(len(items)) +``` + +## Канонический набор overrides + +| Тип операции | Разрешённые overrides | +|---|---| +| read / list / probe | `AvitoSettings.timeouts`, `AvitoSettings.retry_policy` | +| write при `dry_run=False` | `AvitoSettings.timeouts`, `AvitoSettings.retry_policy`, `idempotency_key` | +| write при `dry_run=True` | `AvitoSettings.timeouts` | +| pagination-чтение | `AvitoSettings.timeouts`, `AvitoSettings.retry_policy`, `limit` | + +Полный контракт настроек описан в [reference по конфигурации](../reference/config.md). Правила идемпотентности разобраны в [рецепте по идемпотентности](idempotency.md). diff --git a/docs/site/how-to/security-practices.md b/docs/site/how-to/security-practices.md new file mode 100644 index 0000000..950240a --- /dev/null +++ b/docs/site/how-to/security-practices.md @@ -0,0 +1,91 @@ +# Безопасная работа с SDK + +Этот рецепт фиксирует практики consumer-кода, которые сохраняют security-гарантии SDK: не допускают утечки секретов в логи, исключения и сериализованные данные. + +## Хранение секретов в переменных окружения + +Никогда не хардкодьте `client_id` и `client_secret` в исходном коде. Используйте `.env` или переменные окружения процесса: + +```bash +AVITO_CLIENT_ID=your_client_id +AVITO_CLIENT_SECRET=your_client_secret +``` + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + profile = avito.account().get_self() + +print(profile.user_id) +``` + +SDK читает `AVITO_CLIENT_ID` и `AVITO_CLIENT_SECRET` автоматически. Файл `.env` добавьте в `.gitignore`. + +## Что SDK редактирует автоматически + +SDK гарантирует, что секреты не попадают в диагностику и исключения: + +- `debug_info()` не возвращает `client_secret`, `access_token` или `refresh_token`; +- исключения `AvitoError` редактируют поля `authorization`, `token`, `client_secret` и аналогичные в `metadata` и `headers`; +- строковое представление исключения `str(exc)` содержит только операцию, статус и код ошибки. + +```python +from avito import AvitoClient + +with AvitoClient.from_env() as avito: + info = avito.debug_info() + +print(info.base_url) +print(info.requires_auth) +``` + +## Осторожно с to_dict() и model_dump() + +Публичные SDK-модели реализуют `to_dict()` / `model_dump()` для сериализации. Они могут содержать пользовательские данные (телефон, email, имя). Не передавайте результат напрямую в системы логирования без фильтрации. + +```text +# Небезопасно — логируем всё подряд +profile = avito.account().get_self() +logger.info("Profile: %s", profile.to_dict()) # phone, email попадут в логи + +# Безопасно — логируем только нужные поля +logger.info("User: id=%s, name=%s", profile.user_id, profile.name) +``` + +## user_agent_suffix без секретов + +`user_agent_suffix` идентифицирует интеграцию в HTTP-заголовках. SDK запрещает включать в него секреты: + +```text +# Правильно +settings = AvitoSettings(user_agent_suffix="my-crm/1.0") + +# Ошибка — SDK поднимет ConfigurationError +settings = AvitoSettings(user_agent_suffix="token=abc123") +``` + +## Логирование через debug_info() + +При логировании состояния клиента используйте только `debug_info()`: + +```python +from avito import AvitoClient +import logging + +logger = logging.getLogger(__name__) + +with AvitoClient.from_env() as avito: + info = avito.debug_info() + logger.info( + "Клиент Avito: base_url=%s, retry=%d, timeout_read=%.1f", + info.base_url, + info.retry_max_attempts, + info.timeout_read, + ) + profile = avito.account().get_self() + +print(profile.user_id) +``` + +Security-модель SDK подробно описана в [explanations/security-and-redaction.md](../explanations/security-and-redaction.md). Диагностика и обработка ошибок — в [рецепте по диагностике](diagnostics-and-logging.md). diff --git a/docs/site/how-to/testing-with-fake-transport.md b/docs/site/how-to/testing-with-fake-transport.md new file mode 100644 index 0000000..2b51221 --- /dev/null +++ b/docs/site/how-to/testing-with-fake-transport.md @@ -0,0 +1,124 @@ +# Тестирование с FakeTransport + +`avito.testing` предоставляет `FakeTransport` — детерминированный fake transport для тестов. Он не выполняет реальных HTTP-запросов и позволяет проверять поведение consumer-кода через публичный SDK API. + +## Создание клиента поверх FakeTransport + +`FakeTransport.as_client()` создаёт полностью инициализированный `AvitoClient` без реального HTTP. Используйте его в тестах вместо `AvitoClient.from_env()`. + +```python +from avito.testing import FakeTransport + +fake = FakeTransport() +fake.add_json( + "GET", + "/core/v1/accounts/self", + {"id": 7, "name": "Тест", "email": "test@example.com", "phone": "+7999"}, +) + +with fake.as_client(user_id=7) as avito: + profile = avito.account().get_self() + +print(profile.user_id) +print(profile.name) +``` + +## Скриптование последовательности ответов + +`route_sequence` задаёт несколько ответов для одного маршрута. Ответы расходуются по одному. Последний ответ в очереди переиспользуется. + +```python +from avito.testing import FakeTransport, json_response, route_sequence + +fake = FakeTransport() +fake.add( + "GET", + "/core/v1/accounts/self", + *route_sequence( + json_response({"id": 7, "name": "Тест", "email": "a@b.com", "phone": "+7"}), + json_response({"id": 7, "name": "Обновлён", "email": "a@b.com", "phone": "+7"}), + ), +) + +with fake.as_client(user_id=7) as avito: + first = avito.account().get_self() + second = avito.account().get_self() + +print(first.name) +print(second.name) +``` + +## Инспекция вызовов + +`fake.requests` содержит список `RecordedRequest` со всеми выполненными HTTP-запросами. Используйте его для проверки payload, метода и пути. + +```python +from avito.testing import FakeTransport + +fake = FakeTransport() +fake.add_json( + "GET", + "/core/v1/accounts/self", + {"id": 7, "name": "Тест", "email": "t@e.com", "phone": "+7"}, +) + +with fake.as_client(user_id=7) as avito: + avito.account().get_self() + +assert len(fake.requests) == 1 +req = fake.requests[0] +print(req.method) +print(req.path) +``` + +## Симуляция ошибок transport + +`FakeResponse` позволяет вернуть любой HTTP-статус для проверки обработки ошибок. + +```python +from avito.core.exceptions import AuthenticationError +from avito.testing import FakeResponse, FakeTransport + +fake = FakeTransport() +fake.add( + "GET", + "/core/v1/accounts/self", + FakeResponse(401, json={"error": "unauthorized", "error_description": "token expired"}), +) + +with fake.as_client(user_id=7) as avito: + try: + avito.account().get_self() + except AuthenticationError as exc: + print(exc.status_code) + print(exc.operation) +``` + +## Симуляция rate limit с Retry-After + +Для проверки retry-поведения задайте ответ 429 с заголовком `Retry-After`. SDK учитывает заголовок при расчёте задержки повтора. + +```python +from avito.core.exceptions import RateLimitError +from avito.core.retries import RetryPolicy +from avito.testing import FakeResponse, FakeTransport, json_response, route_sequence + +fake = FakeTransport() +fake.add( + "GET", + "/core/v1/accounts/self", + *route_sequence( + FakeResponse(429, json={"error": "too_many_requests"}, headers={"Retry-After": "1"}), + json_response({"id": 7, "name": "Тест", "email": "t@e.com", "phone": "+7"}), + ), +) + +policy = RetryPolicy(max_attempts=2, max_rate_limit_wait_seconds=2.0) +with fake.as_client(user_id=7, retry_policy=policy) as avito: + profile = avito.account().get_self() + +print(profile.user_id) +print(len(fake.requests)) +``` + +Полный публичный контракт `avito.testing` описан в [reference по тестированию](../reference/testing.md). Стратегия тестирования SDK разобрана в [explanations](../explanations/testing-strategy.md). From 06465025001b2c298f9d62f7cf650f4105eadbaa Mon Sep 17 00:00:00 2001 From: Nikolay Baryshnikov Date: Fri, 24 Apr 2026 13:51:59 +0300 Subject: [PATCH 9/9] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B0?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 +- .github/workflows/ci.yml | 6 + .interrogate-baseline | 98 +++++++++++++ Makefile | 9 ++ avito/testing/fake_transport.py | 18 +++ poetry.lock | 96 ++++++++++++- pyproject.toml | 6 + reference-explanation-examples-report.json | 32 +++++ scripts/check_docs_examples.py | 154 +++++++++++++++++++++ scripts/check_interrogate_gate.py | 114 +++++++++++++++ 10 files changed, 536 insertions(+), 2 deletions(-) create mode 100644 .interrogate-baseline create mode 100644 reference-explanation-examples-report.json create mode 100644 scripts/check_docs_examples.py create mode 100644 scripts/check_interrogate_gate.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 315c4e5..d5287d6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -32,7 +32,10 @@ "Bash(grep -v \"^$\")", "Bash(grep -n \"^\\\\`\\\\`\\\\`\" /Users/n.baryshnikov/Projects/avito_python_api/docs/site/how-to/idempotency.md)", "Bash(grep -n \"^\\\\`\\\\`\\\\`\" /Users/n.baryshnikov/Projects/avito_python_api/docs/site/how-to/per-operation-overrides.md)", - "Bash(grep -n \"^\\\\`\\\\`\\\\`\" /Users/n.baryshnikov/Projects/avito_python_api/docs/site/how-to/security-practices.md)" + "Bash(grep -n \"^\\\\`\\\\`\\\\`\" /Users/n.baryshnikov/Projects/avito_python_api/docs/site/how-to/security-practices.md)", + "Bash(pip show *)", + "Bash(awk -F'|' '{gsub\\(/^ +| +$/,\"\",$2\\); gsub\\(/^ +| +$/,\"\",$6\\); print $2 \":\" $6}')", + "Bash(make qa-docs *)" ] } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2344e04..38260c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,12 @@ jobs: - name: Run docs strict gate run: make docs-strict + - name: Run qa-docs (pydocstyle + interrogate) + run: make qa-docs + + - name: Interrogate diff gate (changed modules vs baseline) + run: poetry run python scripts/check_interrogate_gate.py --base-ref origin/main + - name: Collect coverage report run: | poetry run coverage run -m pytest diff --git a/.interrogate-baseline b/.interrogate-baseline new file mode 100644 index 0000000..0988e5d --- /dev/null +++ b/.interrogate-baseline @@ -0,0 +1,98 @@ +{ + "modules": { + "avito/__init__.py": 100.0, + "avito/__main__.py": 100.0, + "avito/_env.py": 88.0, + "avito/client.py": 92.0, + "avito/config.py": 83.0, + "avito/settings.py": 100.0, + "avito/accounts/__init__.py": 100.0, + "avito/accounts/client.py": 85.0, + "avito/accounts/domain.py": 92.0, + "avito/accounts/enums.py": 100.0, + "avito/accounts/mappers.py": 56.0, + "avito/accounts/models.py": 100.0, + "avito/ads/__init__.py": 100.0, + "avito/ads/client.py": 94.0, + "avito/ads/domain.py": 78.0, + "avito/ads/enums.py": 100.0, + "avito/ads/mappers.py": 68.0, + "avito/ads/models.py": 100.0, + "avito/auth/__init__.py": 100.0, + "avito/auth/enums.py": 100.0, + "avito/auth/mappers.py": 100.0, + "avito/auth/models.py": 100.0, + "avito/auth/provider.py": 60.0, + "avito/auth/settings.py": 100.0, + "avito/autoteka/__init__.py": 100.0, + "avito/autoteka/client.py": 26.0, + "avito/autoteka/domain.py": 91.0, + "avito/autoteka/enums.py": 100.0, + "avito/autoteka/mappers.py": 62.0, + "avito/autoteka/models.py": 100.0, + "avito/core/__init__.py": 100.0, + "avito/core/deprecation.py": 0.0, + "avito/core/domain.py": 100.0, + "avito/core/enums.py": 100.0, + "avito/core/exceptions.py": 84.0, + "avito/core/mapping.py": 100.0, + "avito/core/pagination.py": 41.0, + "avito/core/retries.py": 100.0, + "avito/core/serialization.py": 50.0, + "avito/core/transport.py": 37.0, + "avito/core/types.py": 100.0, + "avito/core/validation.py": 100.0, + "avito/cpa/__init__.py": 100.0, + "avito/cpa/client.py": 30.0, + "avito/cpa/domain.py": 86.0, + "avito/cpa/enums.py": 100.0, + "avito/cpa/mappers.py": 52.0, + "avito/cpa/models.py": 76.0, + "avito/jobs/__init__.py": 100.0, + "avito/jobs/client.py": 19.0, + "avito/jobs/domain.py": 90.0, + "avito/jobs/enums.py": 100.0, + "avito/jobs/mappers.py": 60.0, + "avito/jobs/models.py": 100.0, + "avito/messenger/__init__.py": 100.0, + "avito/messenger/client.py": 100.0, + "avito/messenger/domain.py": 77.0, + "avito/messenger/enums.py": 100.0, + "avito/messenger/mappers.py": 67.0, + "avito/messenger/models.py": 100.0, + "avito/orders/__init__.py": 100.0, + "avito/orders/client.py": 12.0, + "avito/orders/domain.py": 96.0, + "avito/orders/enums.py": 100.0, + "avito/orders/mappers.py": 59.0, + "avito/orders/models.py": 56.0, + "avito/promotion/__init__.py": 100.0, + "avito/promotion/client.py": 100.0, + "avito/promotion/domain.py": 84.0, + "avito/promotion/enums.py": 100.0, + "avito/promotion/mappers.py": 41.0, + "avito/promotion/models.py": 100.0, + "avito/ratings/__init__.py": 100.0, + "avito/ratings/client.py": 33.0, + "avito/ratings/domain.py": 89.0, + "avito/ratings/enums.py": 100.0, + "avito/ratings/mappers.py": 36.0, + "avito/ratings/models.py": 100.0, + "avito/realty/__init__.py": 100.0, + "avito/realty/client.py": 30.0, + "avito/realty/domain.py": 67.0, + "avito/realty/enums.py": 100.0, + "avito/realty/mappers.py": 50.0, + "avito/realty/models.py": 100.0, + "avito/tariffs/__init__.py": 100.0, + "avito/tariffs/client.py": 67.0, + "avito/tariffs/domain.py": 100.0, + "avito/tariffs/enums.py": 100.0, + "avito/tariffs/mappers.py": 22.0, + "avito/tariffs/models.py": 100.0, + "avito/testing/__init__.py": 100.0, + "avito/testing/fake_transport.py": 79.0 + }, + "generated_at": "2026-04-24T09:02:16.576057+00:00", + "interrogate_version": "1.7.0" +} diff --git a/Makefile b/Makefile index 1656abd..dfe19a1 100644 --- a/Makefile +++ b/Makefile @@ -57,8 +57,17 @@ docs-report: poetry run python scripts/check_reference_public_surface.py --output reference-public-report.json poetry run python scripts/check_public_docstrings.py --output docstring-contract-report.json poetry run python scripts/check_changelog_sections.py --output changelog-sections-report.json + poetry run python scripts/check_docs_examples.py --output reference-explanation-examples-report.json poetry run bandit -r avito -lll -f json -o bandit-report.json poetry run python scripts/build_docs_quality_report.py docs-check: docs-strict lychee --exclude "avito\.ru" --retry-wait-time 5 --max-retries 3 --timeout 30 site/ + +qa-docs: + poetry run pydocstyle \ + avito/client.py avito/config.py \ + avito/core/exceptions.py avito/core/pagination.py \ + avito/*/domain.py \ + avito/testing/fake_transport.py + poetry run interrogate avito/ --fail-under=0 --quiet diff --git a/avito/testing/fake_transport.py b/avito/testing/fake_transport.py index 2f8a80c..20124db 100644 --- a/avito/testing/fake_transport.py +++ b/avito/testing/fake_transport.py @@ -1,3 +1,5 @@ +"""Публичный тестовый transport и вспомогательные утилиты для SDK-контрактных тестов.""" + from __future__ import annotations import json @@ -21,6 +23,8 @@ @dataclass(slots=True, frozen=True) class RecordedRequest: + """Зафиксированный HTTP-запрос, перехваченный FakeTransport.""" + method: str path: str params: dict[str, str] @@ -46,6 +50,8 @@ def add( path: str, *responses: RouteResponder, ) -> FakeTransport: + """Регистрирует один или несколько ответов для HTTP-маршрута.""" + key = (method.upper(), path) bucket = self._routes.setdefault(key, deque()) bucket.extend(responses) @@ -60,6 +66,8 @@ def add_json( status_code: int = 200, headers: Mapping[str, str] | None = None, ) -> FakeTransport: + """Регистрирует JSON-ответ для HTTP-маршрута.""" + return self.add( method, path, @@ -76,6 +84,8 @@ def build( retry_policy: RetryPolicy | None = None, user_id: int | None = None, ) -> Transport: + """Создаёт низкоуровневый Transport поверх fake transport (internal helper).""" + settings = AvitoSettings( base_url=self.base_url, user_id=user_id, @@ -124,6 +134,8 @@ def as_client( ) def count(self, *, method: str | None = None, path: str | None = None) -> int: + """Возвращает число перехваченных запросов с опциональной фильтрацией.""" + return len( [ request @@ -134,6 +146,8 @@ def count(self, *, method: str | None = None, path: str | None = None) -> int: ) def last(self, *, method: str | None = None, path: str | None = None) -> RecordedRequest: + """Возвращает последний перехваченный запрос с опциональной фильтрацией.""" + matches = [ request for request in self.requests @@ -187,10 +201,14 @@ def json_response( status_code: int = 200, headers: Mapping[str, str] | None = None, ) -> httpx.Response: + """Создаёт httpx.Response с JSON-телом для использования в FakeTransport.""" + return httpx.Response(status_code, json=payload, headers=dict(headers or {})) def route_sequence(*responses: RouteResponder) -> Iterable[RouteResponder]: + """Упаковывает несколько ответов в последовательность для FakeTransport.add().""" + return responses diff --git a/poetry.lock b/poetry.lock index 5dc2ed4..e66cb88 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,6 +18,18 @@ idna = ">=2.8" [package.extras] trio = ["trio (>=0.32.0)"] +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + [[package]] name = "babel" version = "2.18.0" @@ -507,6 +519,31 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "interrogate" +version = "1.7.0" +description = "Interrogate a codebase for docstring coverage." +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12"}, + {file = "interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0"}, +] + +[package.dependencies] +attrs = "*" +click = ">=7.1" +colorama = "*" +py = "*" +tabulate = "*" + +[package.extras] +dev = ["cairosvg", "coverage[toml]", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "sphinx", "sphinx-autobuild", "wheel"] +docs = ["sphinx", "sphinx-autobuild"] +png = ["cairosvg"] +tests = ["coverage[toml]", "pytest", "pytest-cov", "pytest-mock"] + [[package]] name = "jinja2" version = "3.1.6" @@ -1245,6 +1282,36 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["docs"] +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +groups = ["docs"] +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] + [[package]] name = "pygments" version = "2.20.0" @@ -1526,6 +1593,18 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" +groups = ["docs"] +files = [ + {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, + {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, +] + [[package]] name = "stevedore" version = "5.7.0" @@ -1538,6 +1617,21 @@ files = [ {file = "stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3"}, ] +[[package]] +name = "tabulate" +version = "0.10.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3"}, + {file = "tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d"}, +] + +[package.extras] +widechars = ["wcwidth"] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1644,4 +1738,4 @@ bracex = ">=2.1.1" [metadata] lock-version = "2.1" python-versions = "^3.14" -content-hash = "9d93e82c7bbd2549c9184ae2dcb60760017ee6281063865726e5efd92e634565" +content-hash = "4e9624b5ae0a403e682c467ed725140d84f73f5ba58c87809fa079684f550a80" diff --git a/pyproject.toml b/pyproject.toml index 287e4a1..2b69b22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,8 @@ mkdocs-gen-files = ">=0.5" mkdocs-literate-nav = ">=0.6" mktestdocs = "^0.2.5" bandit = ">=1.7" +interrogate = ">=1.7" +pydocstyle = { version = ">=6.3", extras = ["toml"] } [tool.pytest.ini_options] testpaths = ["tests"] @@ -87,6 +89,10 @@ ignore = [ quote-style = "double" indent-style = "space" +[tool.pydocstyle] +convention = "google" +add_ignore = ["D202", "D105", "D107"] + [build-system] requires = ["poetry-core>=1.9.0"] build-backend = "poetry.core.masonry.api" diff --git a/reference-explanation-examples-report.json b/reference-explanation-examples-report.json new file mode 100644 index 0000000..7e28c87 --- /dev/null +++ b/reference-explanation-examples-report.json @@ -0,0 +1,32 @@ +{ + "checked_dirs": [ + "reference", + "explanations" + ], + "executable_sources": [ + "README.md", + "docs/site/tutorials/first-promotion.md", + "docs/site/tutorials/getting-started.md", + "docs/site/tutorials/index.md", + "docs/site/how-to/account-profile.md", + "docs/site/how-to/ad-listing-and-stats.md", + "docs/site/how-to/auth-and-config.md", + "docs/site/how-to/autoteka-report.md", + "docs/site/how-to/chat-image-upload.md", + "docs/site/how-to/cpa-calltracking.md", + "docs/site/how-to/diagnostics-and-logging.md", + "docs/site/how-to/idempotency.md", + "docs/site/how-to/index.md", + "docs/site/how-to/job-applications.md", + "docs/site/how-to/order-labels.md", + "docs/site/how-to/pagination.md", + "docs/site/how-to/per-operation-overrides.md", + "docs/site/how-to/promotion-dry-run.md", + "docs/site/how-to/ratings-and-tariffs.md", + "docs/site/how-to/realty-booking.md", + "docs/site/how-to/security-practices.md", + "docs/site/how-to/testing-with-fake-transport.md" + ], + "gap_count": 0, + "gaps": [] +} \ No newline at end of file diff --git a/scripts/check_docs_examples.py b/scripts/check_docs_examples.py new file mode 100644 index 0000000..14da32b --- /dev/null +++ b/scripts/check_docs_examples.py @@ -0,0 +1,154 @@ +"""Проверяет, что python/pycon блоки в reference/ и explanations/ не являются «orphaned». + +Orphaned блок — fenced code block с меткой `python` или `pycon`, который находится в +`docs/site/reference/` или `docs/site/explanations/` и НЕ включён в mktestdocs-сборщик +(README.md, tutorials/*.md, how-to/*.md). + +По умолчанию такие блоки запрещены: если блок показывает SDK-вызов, он должен либо +исполняться через тот же harness, либо быть помечен нейтральным fence (text, console и т.д.). + +Использование: + python scripts/check_docs_examples.py [--output report.json] [--strict] +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +DOCS_ROOT = Path("docs/site") +CHECKED_DIRS = ["reference", "explanations"] +EXECUTABLE_DIRS = ["tutorials", "how-to"] +EXECUTABLE_FILES = ["README.md"] + +EXECUTABLE_FENCE = re.compile(r"^```(python|pycon)\s*$", re.MULTILINE) +ANY_FENCE_OPEN = re.compile(r"^```(\S*)\s*$", re.MULTILINE) + + +def collect_executable_fences(paths: list[Path]) -> set[str]: + """Собирает содержимое python/pycon блоков из executable-файлов.""" + + blocks: set[str] = set() + for path in paths: + if not path.exists(): + continue + text = path.read_text(encoding="utf-8") + for block in extract_fenced_blocks(text, {"python", "pycon"}): + blocks.add(block.strip()) + return blocks + + +def extract_fenced_blocks(text: str, fence_types: set[str]) -> list[str]: + """Извлекает содержимое fenced-блоков заданных типов.""" + + blocks: list[str] = [] + lines = text.splitlines() + in_block = False + current_type = "" + current_lines: list[str] = [] + + for line in lines: + if not in_block: + m = re.match(r"^```(\S*)\s*$", line) + if m: + fence_type = m.group(1).lower() + if fence_type in fence_types: + in_block = True + current_type = fence_type + current_lines = [] + else: + if line.strip() == "```": + blocks.append("\n".join(current_lines)) + in_block = False + current_type = "" + current_lines = [] + else: + current_lines.append(line) + + return blocks + + +def find_orphaned_blocks( + checked_dirs: list[Path], + executable_blocks: set[str], +) -> list[dict[str, object]]: + """Находит python/pycon блоки в reference/explanations, не покрытые harness.""" + + gaps: list[dict[str, object]] = [] + for directory in checked_dirs: + if not directory.exists(): + continue + for md_file in sorted(directory.rglob("*.md")): + text = md_file.read_text(encoding="utf-8") + blocks = extract_fenced_blocks(text, {"python", "pycon"}) + for block in blocks: + if block.strip() not in executable_blocks: + gaps.append( + { + "file": str(md_file), + "block_preview": block.strip()[:120], + } + ) + return gaps + + +def main() -> int: + """Запускает проверку и возвращает код выхода.""" + + parser = argparse.ArgumentParser( + description="Проверяет orphaned python-блоки в reference/explanations" + ) + parser.add_argument("--output", default=None, help="Путь для JSON-отчёта") + parser.add_argument( + "--strict", + action="store_true", + help="Завершиться с кодом 1 при наличии gaps", + ) + args = parser.parse_args() + + executable_paths: list[Path] = [] + for name in EXECUTABLE_FILES: + executable_paths.append(Path(name)) + for d in EXECUTABLE_DIRS: + executable_paths.extend(sorted((DOCS_ROOT / d).rglob("*.md"))) + + executable_blocks = collect_executable_fences(executable_paths) + + checked_dirs = [DOCS_ROOT / d for d in CHECKED_DIRS] + gaps = find_orphaned_blocks(checked_dirs, executable_blocks) + + report = { + "checked_dirs": CHECKED_DIRS, + "executable_sources": [str(p) for p in executable_paths if p.exists()], + "gap_count": len(gaps), + "gaps": gaps, + } + + if args.output: + Path(args.output).write_text(json.dumps(report, indent=2, ensure_ascii=False)) + print(f"Отчёт сохранён в {args.output}") + + if gaps: + print( + f"Найдено {len(gaps)} orphaned python/pycon блок(а/ов) " + "в reference/ или explanations/:" + ) + for g in gaps: + print(f" {g['file']}") + print(f" {g['block_preview']!r}") + if args.strict: + return 1 + else: + print( + f"reference_explanation_examples_gaps=0 " + f"(проверено {len(checked_dirs)} директорий)" + ) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_interrogate_gate.py b/scripts/check_interrogate_gate.py new file mode 100644 index 0000000..c152520 --- /dev/null +++ b/scripts/check_interrogate_gate.py @@ -0,0 +1,114 @@ +"""Interrogate diff-gate: проверяет, что изменённые модули не ухудшили покрытие docstrings. + +Использование: + python scripts/check_interrogate_gate.py [--baseline .interrogate-baseline] [--base-ref origin/main] + +Сравнивает текущее покрытие docstrings в каждом изменённом avito/*.py модуле с +зафиксированным baseline. Завершается с ненулевым кодом, если покрытие упало. +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path + + +def get_changed_modules(base_ref: str) -> list[str]: + """Возвращает список изменённых .py файлов в avito/ по сравнению с base_ref.""" + + result = subprocess.run( + ["git", "diff", "--name-only", base_ref], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print(f"Предупреждение: git diff завершился с кодом {result.returncode}", file=sys.stderr) + return [] + return [ + line.strip() + for line in result.stdout.splitlines() + if line.strip().startswith("avito/") and line.strip().endswith(".py") + ] + + +def get_module_coverage(module_path: str) -> float | None: + """Запускает interrogate для одного файла и возвращает процент покрытия.""" + + result = subprocess.run( + ["poetry", "run", "interrogate", module_path, "--fail-under=0", "-vv"], + capture_output=True, + text=True, + check=False, + ) + output = result.stdout + result.stderr + basename = Path(module_path).name + # Ищем строку с именем файла в Summary-таблице (целое %; совпадает с baseline). + match = re.search( + r"\|\s+" + re.escape(basename) + r"\s+\|\s+\d+\s+\|\s+\d+\s+\|\s+\d+\s+\|\s+(\d+)%", + output, + ) + if match: + return float(match.group(1)) + return None + + +def load_baseline(baseline_path: Path) -> dict[str, float]: + """Загружает baseline из JSON-файла.""" + + if not baseline_path.exists(): + return {} + with baseline_path.open() as f: + data = json.load(f) + return {k: float(v) for k, v in data.get("modules", {}).items()} + + +def main() -> int: + """Запускает interrogate diff-gate и возвращает код выхода.""" + + parser = argparse.ArgumentParser(description="Interrogate diff-gate против baseline") + parser.add_argument("--baseline", default=".interrogate-baseline", help="Путь к baseline-файлу") + parser.add_argument("--base-ref", default="origin/main", help="Git ref для сравнения") + args = parser.parse_args() + + baseline = load_baseline(Path(args.baseline)) + changed = get_changed_modules(args.base_ref) + + if not changed: + print("Нет изменённых avito/ модулей — gate пройден.") + return 0 + + failures: list[str] = [] + for module in changed: + current = get_module_coverage(module) + if current is None: + print(f" ПРОПУСК {module}: не удалось получить покрытие") + continue + + baseline_value = baseline.get(module) + if baseline_value is None: + print(f" НОВЫЙ {module}: {current:.0f}% (не в baseline)") + continue + + delta = current - baseline_value + status = "OK" if delta >= 0 else "УПАЛО" + print(f" {status:6s} {module}: {current:.0f}% (baseline {baseline_value:.0f}%, delta {delta:+.0f}%)") + if delta < 0: + failures.append(f"{module}: {current:.0f}% < baseline {baseline_value:.0f}%") + + if failures: + print(f"\nGate провален — покрытие упало в {len(failures)} модуле(ях):") + for f in failures: + print(f" - {f}") + return 1 + + print(f"\nGate пройден — {len(changed)} изменённых модулей, регрессий нет.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())