python3-capsolver 1.2.0__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {python3_capsolver-1.2.0/src/python3_capsolver.egg-info → python3_capsolver-1.2.2}/PKG-INFO +37 -7
  2. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/README.md +33 -5
  3. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/pyproject.toml +3 -1
  4. python3_capsolver-1.2.2/src/python3_capsolver/__version__.py +1 -0
  5. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/aio_captcha_instrument.py +8 -3
  6. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/sio_captcha_instrument.py +8 -3
  7. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2/src/python3_capsolver.egg-info}/PKG-INFO +37 -7
  8. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver.egg-info/SOURCES.txt +2 -0
  9. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver.egg-info/requires.txt +1 -1
  10. python3_capsolver-1.2.2/tests/test_aio_captcha_instrument.py +186 -0
  11. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_instrument.py +16 -2
  12. python3_capsolver-1.2.2/tests/test_sio_captcha_instrument.py +185 -0
  13. python3_capsolver-1.2.0/src/python3_capsolver/__version__.py +0 -1
  14. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/LICENSE +0 -0
  15. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/MANIFEST.in +0 -0
  16. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/setup.cfg +0 -0
  17. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/__init__.py +0 -0
  18. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/aws_waf.py +0 -0
  19. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/cloudflare.py +0 -0
  20. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/control.py +0 -0
  21. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/__init__.py +0 -0
  22. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/base.py +0 -0
  23. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/captcha_instrument.py +0 -0
  24. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/const.py +0 -0
  25. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/context_instr.py +0 -0
  26. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/enum.py +0 -0
  27. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/serializer.py +0 -0
  28. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/core/utils.py +0 -0
  29. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/datadome_slider.py +0 -0
  30. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/friendly_captcha.py +0 -0
  31. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/gee_test.py +0 -0
  32. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/image_to_text.py +0 -0
  33. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/mt_captcha.py +0 -0
  34. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/recaptcha.py +0 -0
  35. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/vision_engine.py +0 -0
  36. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver/yandex.py +0 -0
  37. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver.egg-info/dependency_links.txt +0 -0
  38. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/src/python3_capsolver.egg-info/top_level.txt +0 -0
  39. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_aws_waf.py +0 -0
  40. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_cloudflare.py +0 -0
  41. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_control.py +0 -0
  42. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_core.py +0 -0
  43. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_datadome.py +0 -0
  44. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_friendly.py +0 -0
  45. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_gee_test.py +0 -0
  46. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_image_to_text.py +0 -0
  47. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_mt_captcha.py +0 -0
  48. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_recaptcha.py +0 -0
  49. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_vision_engine.py +0 -0
  50. {python3_capsolver-1.2.0 → python3_capsolver-1.2.2}/tests/test_yandex.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-capsolver
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Python 3.8+ Capsolver library with AIO module.
5
5
  Author-email: AndreiDrang <python-captcha@pm.me>
6
6
  License: MIT License
@@ -21,6 +21,8 @@ Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
23
  Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
24
26
  Classifier: Framework :: AsyncIO
25
27
  Classifier: Operating System :: Unix
26
28
  Classifier: Operating System :: Microsoft :: Windows
@@ -30,7 +32,7 @@ Description-Content-Type: text/markdown
30
32
  License-File: LICENSE
31
33
  Requires-Dist: requests>=2.21.0
32
34
  Requires-Dist: aiohttp>=3.9.2
33
- Requires-Dist: msgspec<=0.21,>=0.18
35
+ Requires-Dist: msgspec>=0.18
34
36
  Requires-Dist: tenacity<10,>=8
35
37
  Dynamic: license-file
36
38
 
@@ -62,6 +64,7 @@ Tested on UNIX based OS.
62
64
  The library is intended for software developers and is used to work with the [Capsolver](https://dashboard.capsolver.com/passport/register?inviteCode=kQTn-tG07Jb1) service API.
63
65
 
64
66
  ## Features
67
+ - **Modern Tooling**: Uses `uv` for fast, reliable dependency management and environment isolation.
65
68
  - **Sync & Async Support**: Full support for both synchronous (`requests`) and asynchronous (`aiohttp`) operations.
66
69
  - **Type Safety**: Enums for captcha types and response statuses.
67
70
  - **Resilience**: Built-in retries using `tenacity`.
@@ -70,9 +73,21 @@ The library is intended for software developers and is used to work with the [Ca
70
73
 
71
74
  ## How to install?
72
75
 
73
- We recommend using the latest version of Python. `python3-capsolver` supports Python 3.7+.
76
+ We recommend using the latest version of Python. `python3-capsolver` supports Python 3.8+.
74
77
 
75
- ### pip
78
+ ### Development (using uv)
79
+
80
+ This project uses [uv](https://github.com/astral-sh/uv) for fast, reliable dependency management.
81
+
82
+ ```bash
83
+ # Install uv (if not already installed)
84
+ curl -LsSf https://astral.sh/uv/install.sh | sh
85
+
86
+ # Install project dependencies
87
+ uv sync --all-groups
88
+ ```
89
+
90
+ ### Production (pip)
76
91
 
77
92
  ```bash
78
93
  pip install python3-capsolver
@@ -151,13 +166,28 @@ if __name__ == "__main__":
151
166
 
152
167
  ## How to test?
153
168
 
154
- 1. You need set ``API_KEY`` in your environment(get this value from you account).
155
- 2. Run command ``make tests``, from root directory.
169
+ The project uses `uv` for testing and development tasks.
170
+
171
+ ```bash
172
+ # 1. Set API_KEY in your environment (get this value from your account)
173
+ export API_KEY="your_api_key_here"
174
+
175
+ # 2. Run tests (uses uv internally)
176
+ make tests
177
+
178
+ # Other useful make targets:
179
+ # make install # Install dependencies with uv sync
180
+ # make lint # Run linting checks
181
+ # make build # Build the package
182
+ # make doc # Generate documentation
183
+ ```
184
+
185
+ All `make` commands automatically use `uv` to run commands in the isolated environment.
156
186
 
157
187
 
158
188
  ### Changelog
159
189
 
160
- Check [releases page](https://github.com/AndreiDrang/python3-capsolver/releases).
190
+ See [CHANGELOG.md](https://github.com/AndreiDrang/python3-capsolver/blob/main/CHANGELOG.md) for version history and detailed changes.
161
191
 
162
192
  ### How to get API Key to work with the library
163
193
  1. On the page - https://dashboard.capsolver.com/overview/user-center
@@ -26,6 +26,7 @@ Tested on UNIX based OS.
26
26
  The library is intended for software developers and is used to work with the [Capsolver](https://dashboard.capsolver.com/passport/register?inviteCode=kQTn-tG07Jb1) service API.
27
27
 
28
28
  ## Features
29
+ - **Modern Tooling**: Uses `uv` for fast, reliable dependency management and environment isolation.
29
30
  - **Sync & Async Support**: Full support for both synchronous (`requests`) and asynchronous (`aiohttp`) operations.
30
31
  - **Type Safety**: Enums for captcha types and response statuses.
31
32
  - **Resilience**: Built-in retries using `tenacity`.
@@ -34,9 +35,21 @@ The library is intended for software developers and is used to work with the [Ca
34
35
 
35
36
  ## How to install?
36
37
 
37
- We recommend using the latest version of Python. `python3-capsolver` supports Python 3.7+.
38
+ We recommend using the latest version of Python. `python3-capsolver` supports Python 3.8+.
38
39
 
39
- ### pip
40
+ ### Development (using uv)
41
+
42
+ This project uses [uv](https://github.com/astral-sh/uv) for fast, reliable dependency management.
43
+
44
+ ```bash
45
+ # Install uv (if not already installed)
46
+ curl -LsSf https://astral.sh/uv/install.sh | sh
47
+
48
+ # Install project dependencies
49
+ uv sync --all-groups
50
+ ```
51
+
52
+ ### Production (pip)
40
53
 
41
54
  ```bash
42
55
  pip install python3-capsolver
@@ -115,13 +128,28 @@ if __name__ == "__main__":
115
128
 
116
129
  ## How to test?
117
130
 
118
- 1. You need set ``API_KEY`` in your environment(get this value from you account).
119
- 2. Run command ``make tests``, from root directory.
131
+ The project uses `uv` for testing and development tasks.
132
+
133
+ ```bash
134
+ # 1. Set API_KEY in your environment (get this value from your account)
135
+ export API_KEY="your_api_key_here"
136
+
137
+ # 2. Run tests (uses uv internally)
138
+ make tests
139
+
140
+ # Other useful make targets:
141
+ # make install # Install dependencies with uv sync
142
+ # make lint # Run linting checks
143
+ # make build # Build the package
144
+ # make doc # Generate documentation
145
+ ```
146
+
147
+ All `make` commands automatically use `uv` to run commands in the isolated environment.
120
148
 
121
149
 
122
150
  ### Changelog
123
151
 
124
- Check [releases page](https://github.com/AndreiDrang/python3-capsolver/releases).
152
+ See [CHANGELOG.md](https://github.com/AndreiDrang/python3-capsolver/blob/main/CHANGELOG.md) for version history and detailed changes.
125
153
 
126
154
  ### How to get API Key to work with the library
127
155
  1. On the page - https://dashboard.capsolver.com/overview/user-center
@@ -78,6 +78,8 @@ classifiers = [
78
78
  "Programming Language :: Python :: 3.10",
79
79
  "Programming Language :: Python :: 3.11",
80
80
  "Programming Language :: Python :: 3.12",
81
+ "Programming Language :: Python :: 3.13",
82
+ "Programming Language :: Python :: 3.14",
81
83
  "Framework :: AsyncIO",
82
84
  "Operating System :: Unix",
83
85
  "Operating System :: Microsoft :: Windows",
@@ -86,7 +88,7 @@ classifiers = [
86
88
  dependencies = [
87
89
  "requests>=2.21.0",
88
90
  "aiohttp>=3.9.2",
89
- "msgspec>=0.18,<=0.21",
91
+ "msgspec>=0.18",
90
92
  "tenacity>=8,<10"
91
93
  ]
92
94
 
@@ -0,0 +1 @@
1
+ __version__ = "1.2.2"
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import logging
3
- from typing import Optional
5
+ from typing import TYPE_CHECKING
4
6
  from urllib import parse
5
7
 
6
8
  import aiohttp
@@ -11,6 +13,9 @@ from .utils import attempts_generator
11
13
  from .serializer import CaptchaResponseSer
12
14
  from .captcha_instrument import CaptchaInstrumentBase
13
15
 
16
+ if TYPE_CHECKING:
17
+ from .base import CaptchaParams
18
+
14
19
  __all__ = ("AIOCaptchaInstrument",)
15
20
 
16
21
 
@@ -19,7 +24,7 @@ class AIOCaptchaInstrument(CaptchaInstrumentBase):
19
24
  Instrument for working with async captcha
20
25
  """
21
26
 
22
- def __init__(self, captcha_params: "CaptchaParams"):
27
+ def __init__(self, captcha_params: CaptchaParams):
23
28
  super().__init__()
24
29
  self.captcha_params = captcha_params
25
30
  self.created_task_data = CaptchaResponseSer
@@ -99,7 +104,7 @@ class AIOCaptchaInstrument(CaptchaInstrumentBase):
99
104
 
100
105
  @staticmethod
101
106
  async def send_post_request(
102
- payload: Optional[dict] = None,
107
+ payload: dict | None = None,
103
108
  url_postfix: EndpointPostfixEnm = EndpointPostfixEnm.GET_BALANCE,
104
109
  ) -> dict:
105
110
  """
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
4
  import logging
3
- from typing import Optional
5
+ from typing import TYPE_CHECKING
4
6
  from urllib import parse
5
7
 
6
8
  import requests
@@ -12,6 +14,9 @@ from .utils import attempts_generator
12
14
  from .serializer import CaptchaResponseSer
13
15
  from .captcha_instrument import CaptchaInstrumentBase
14
16
 
17
+ if TYPE_CHECKING:
18
+ from .base import CaptchaParams
19
+
15
20
  __all__ = ("SIOCaptchaInstrument",)
16
21
 
17
22
 
@@ -20,7 +25,7 @@ class SIOCaptchaInstrument(CaptchaInstrumentBase):
20
25
  Instrument for working with sync captcha
21
26
  """
22
27
 
23
- def __init__(self, captcha_params: "CaptchaParams"):
28
+ def __init__(self, captcha_params: CaptchaParams):
24
29
  super().__init__()
25
30
  self.captcha_params = captcha_params
26
31
  self.created_task_data = CaptchaResponseSer
@@ -106,7 +111,7 @@ class SIOCaptchaInstrument(CaptchaInstrumentBase):
106
111
 
107
112
  @staticmethod
108
113
  def send_post_request(
109
- payload: Optional[dict] = None,
114
+ payload: dict | None = None,
110
115
  session: requests.Session = requests.Session(),
111
116
  url_postfix: EndpointPostfixEnm = EndpointPostfixEnm.GET_BALANCE,
112
117
  ) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-capsolver
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Python 3.8+ Capsolver library with AIO module.
5
5
  Author-email: AndreiDrang <python-captcha@pm.me>
6
6
  License: MIT License
@@ -21,6 +21,8 @@ Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
23
  Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
24
26
  Classifier: Framework :: AsyncIO
25
27
  Classifier: Operating System :: Unix
26
28
  Classifier: Operating System :: Microsoft :: Windows
@@ -30,7 +32,7 @@ Description-Content-Type: text/markdown
30
32
  License-File: LICENSE
31
33
  Requires-Dist: requests>=2.21.0
32
34
  Requires-Dist: aiohttp>=3.9.2
33
- Requires-Dist: msgspec<=0.21,>=0.18
35
+ Requires-Dist: msgspec>=0.18
34
36
  Requires-Dist: tenacity<10,>=8
35
37
  Dynamic: license-file
36
38
 
@@ -62,6 +64,7 @@ Tested on UNIX based OS.
62
64
  The library is intended for software developers and is used to work with the [Capsolver](https://dashboard.capsolver.com/passport/register?inviteCode=kQTn-tG07Jb1) service API.
63
65
 
64
66
  ## Features
67
+ - **Modern Tooling**: Uses `uv` for fast, reliable dependency management and environment isolation.
65
68
  - **Sync & Async Support**: Full support for both synchronous (`requests`) and asynchronous (`aiohttp`) operations.
66
69
  - **Type Safety**: Enums for captcha types and response statuses.
67
70
  - **Resilience**: Built-in retries using `tenacity`.
@@ -70,9 +73,21 @@ The library is intended for software developers and is used to work with the [Ca
70
73
 
71
74
  ## How to install?
72
75
 
73
- We recommend using the latest version of Python. `python3-capsolver` supports Python 3.7+.
76
+ We recommend using the latest version of Python. `python3-capsolver` supports Python 3.8+.
74
77
 
75
- ### pip
78
+ ### Development (using uv)
79
+
80
+ This project uses [uv](https://github.com/astral-sh/uv) for fast, reliable dependency management.
81
+
82
+ ```bash
83
+ # Install uv (if not already installed)
84
+ curl -LsSf https://astral.sh/uv/install.sh | sh
85
+
86
+ # Install project dependencies
87
+ uv sync --all-groups
88
+ ```
89
+
90
+ ### Production (pip)
76
91
 
77
92
  ```bash
78
93
  pip install python3-capsolver
@@ -151,13 +166,28 @@ if __name__ == "__main__":
151
166
 
152
167
  ## How to test?
153
168
 
154
- 1. You need set ``API_KEY`` in your environment(get this value from you account).
155
- 2. Run command ``make tests``, from root directory.
169
+ The project uses `uv` for testing and development tasks.
170
+
171
+ ```bash
172
+ # 1. Set API_KEY in your environment (get this value from your account)
173
+ export API_KEY="your_api_key_here"
174
+
175
+ # 2. Run tests (uses uv internally)
176
+ make tests
177
+
178
+ # Other useful make targets:
179
+ # make install # Install dependencies with uv sync
180
+ # make lint # Run linting checks
181
+ # make build # Build the package
182
+ # make doc # Generate documentation
183
+ ```
184
+
185
+ All `make` commands automatically use `uv` to run commands in the isolated environment.
156
186
 
157
187
 
158
188
  ### Changelog
159
189
 
160
- Check [releases page](https://github.com/AndreiDrang/python3-capsolver/releases).
190
+ See [CHANGELOG.md](https://github.com/AndreiDrang/python3-capsolver/blob/main/CHANGELOG.md) for version history and detailed changes.
161
191
 
162
192
  ### How to get API Key to work with the library
163
193
  1. On the page - https://dashboard.capsolver.com/overview/user-center
@@ -30,6 +30,7 @@ src/python3_capsolver/core/enum.py
30
30
  src/python3_capsolver/core/serializer.py
31
31
  src/python3_capsolver/core/sio_captcha_instrument.py
32
32
  src/python3_capsolver/core/utils.py
33
+ tests/test_aio_captcha_instrument.py
33
34
  tests/test_aws_waf.py
34
35
  tests/test_cloudflare.py
35
36
  tests/test_control.py
@@ -41,5 +42,6 @@ tests/test_image_to_text.py
41
42
  tests/test_instrument.py
42
43
  tests/test_mt_captcha.py
43
44
  tests/test_recaptcha.py
45
+ tests/test_sio_captcha_instrument.py
44
46
  tests/test_vision_engine.py
45
47
  tests/test_yandex.py
@@ -1,4 +1,4 @@
1
1
  requests>=2.21.0
2
2
  aiohttp>=3.9.2
3
- msgspec<=0.21,>=0.18
3
+ msgspec>=0.18
4
4
  tenacity<10,>=8
@@ -0,0 +1,186 @@
1
+ from unittest.mock import AsyncMock, MagicMock, patch
2
+
3
+ import pytest
4
+
5
+ from tests.conftest import BaseTest
6
+ from python3_capsolver.core.base import CaptchaParams
7
+ from python3_capsolver.core.enum import CaptchaTypeEnm, EndpointPostfixEnm
8
+ from python3_capsolver.core.const import REQUEST_URL
9
+ from python3_capsolver.core.aio_captcha_instrument import AIOCaptchaInstrument
10
+
11
+ API_KEY_STUB = "unit-test-api-key-000000000000000000000000"
12
+ CREATE_TASK_URL = "https://unit.test/createTask"
13
+ GET_TASK_RESULT_URL = "https://unit.test/getTaskResult"
14
+
15
+
16
+ def create_captcha_params() -> CaptchaParams:
17
+ """
18
+ Build params pointing to a fake API host with zero sleep between polls:
19
+ no real request can leave the mocked `aiohttp.ClientSession.post` boundary
20
+ """
21
+ return CaptchaParams(
22
+ api_key=API_KEY_STUB,
23
+ captcha_type=CaptchaTypeEnm.Control,
24
+ sleep_time=0,
25
+ request_url="https://unit.test/",
26
+ )
27
+
28
+
29
+ def aio_response(payload=None, status: int = 200, reason: str = "unit test failure") -> MagicMock:
30
+ """
31
+ Build fake response for the `aiohttp` boundary, usable as async context manager.
32
+
33
+ For non-parsable bodies (``payload is None``) any ``json()`` call raises -
34
+ an error status must never be parsed as a success body
35
+ """
36
+ response = MagicMock()
37
+ response.status = status
38
+ response.reason = reason
39
+ if payload is None:
40
+ response.json = AsyncMock(side_effect=AssertionError("body of an error response must not be parsed"))
41
+ else:
42
+ response.json = AsyncMock(return_value=payload)
43
+ response.__aenter__.return_value = response
44
+ return response
45
+
46
+
47
+ class TestAIOCaptchaInstrument(BaseTest):
48
+ """
49
+ Mocked unit tests for AIOCaptchaInstrument
50
+ """
51
+
52
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
53
+ async def test_returns_created_result_without_polling_when_task_is_ready(self, mock_post):
54
+ mock_post.return_value = aio_response(
55
+ {"errorId": 0, "taskId": "task-ready-1", "status": "ready", "solution": {"text": "wg6x"}}
56
+ )
57
+
58
+ result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
59
+
60
+ assert result["errorId"] == 0
61
+ assert result["status"] == "ready"
62
+ assert result["taskId"] == "task-ready-1"
63
+ assert result["solution"] == {"text": "wg6x"}
64
+ # task was solved at creation - no `getTaskResult` polling must follow
65
+ mock_post.assert_called_once()
66
+ assert mock_post.call_args.args[0] == CREATE_TASK_URL
67
+ assert mock_post.call_args.kwargs["json"]["task"] == {"type": "Control"}
68
+ assert mock_post.call_args.kwargs["json"]["clientKey"] == API_KEY_STUB
69
+
70
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
71
+ async def test_polls_get_task_result_until_ready(self, mock_post):
72
+ mock_post.side_effect = [
73
+ aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
74
+ aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
75
+ aio_response({"errorId": 0, "taskId": "task-42", "status": "ready", "solution": {"text": "ab12"}}),
76
+ ]
77
+
78
+ result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
79
+
80
+ assert result["errorId"] == 0
81
+ assert result["status"] == "ready"
82
+ assert result["solution"] == {"text": "ab12"}
83
+ assert [call.args[0] for call in mock_post.call_args_list] == [
84
+ CREATE_TASK_URL,
85
+ GET_TASK_RESULT_URL,
86
+ GET_TASK_RESULT_URL,
87
+ ]
88
+ # created taskId must be propagated to every polling request
89
+ assert mock_post.call_args_list[1].kwargs["json"]["taskId"] == "task-42"
90
+ assert mock_post.call_args_list[2].kwargs["json"]["taskId"] == "task-42"
91
+
92
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
93
+ async def test_returns_failed_result_when_polling_reports_failure(self, mock_post):
94
+ mock_post.side_effect = [
95
+ aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
96
+ aio_response({"errorId": 1, "errorCode": "ERROR_TASK_NOT_FOUND", "status": "failed"}),
97
+ ]
98
+
99
+ result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
100
+
101
+ assert result["errorId"] == 1
102
+ assert result["status"] == "failed"
103
+ assert result["errorCode"] == "ERROR_TASK_NOT_FOUND"
104
+ assert len(mock_post.call_args_list) == 2
105
+
106
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
107
+ async def test_marks_result_failed_when_task_creation_is_rejected(self, mock_post):
108
+ mock_post.return_value = aio_response(
109
+ {"errorId": 1, "errorCode": "ERROR_KEY_DOES_NOT_EXIST", "errorDescription": "Client key is invalid"}
110
+ )
111
+
112
+ result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
113
+
114
+ assert result["errorId"] == 1
115
+ assert result["status"] == "failed"
116
+ assert result["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST"
117
+ assert result["errorDescription"] == "Client key is invalid"
118
+ # rejected creation must not trigger result polling
119
+ mock_post.assert_called_once()
120
+
121
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
122
+ async def test_parses_error_payload_when_status_code_is_valid_error_status(self, mock_post):
123
+ mock_post.return_value = aio_response({"errorId": 1, "errorCode": "ERROR_WRONG_USERKEY"}, status=400)
124
+
125
+ result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
126
+
127
+ assert result["status"] == "failed"
128
+ assert result["errorCode"] == "ERROR_WRONG_USERKEY"
129
+
130
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
131
+ async def test_returns_unsolvable_default_when_server_never_finishes(self, mock_post):
132
+ mock_post.side_effect = [aio_response({"errorId": 0, "taskId": "task-silent", "status": "processing"})] * 16
133
+
134
+ result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
135
+
136
+ assert result["errorId"] == 1
137
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
138
+ assert result["errorDescription"] == "Captcha not recognized"
139
+ assert result["status"] == "failed"
140
+ assert result["taskId"] == "task-silent"
141
+ # 1 `createTask` + 15 polling attempts, then the instrument gives up
142
+ assert len(mock_post.call_args_list) == 16
143
+
144
+ @pytest.mark.parametrize("bad_status", [404, 500])
145
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
146
+ async def test_raises_when_create_task_returns_invalid_status(self, mock_post, bad_status):
147
+ mock_post.return_value = aio_response(status=bad_status, reason="Internal Server Error")
148
+
149
+ with pytest.raises(ValueError):
150
+ await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
151
+
152
+ mock_post.assert_called_once()
153
+
154
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
155
+ async def test_raises_when_get_task_result_returns_invalid_status(self, mock_post):
156
+ mock_post.side_effect = [
157
+ aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
158
+ aio_response(status=503, reason="Service Unavailable"),
159
+ ]
160
+
161
+ with pytest.raises(ValueError):
162
+ await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
163
+
164
+ assert len(mock_post.call_args_list) == 2
165
+
166
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
167
+ async def test_send_post_request_returns_decoded_payload(self, mock_post):
168
+ mock_post.return_value = aio_response({"errorId": 0, "balance": 107.7})
169
+
170
+ result = await AIOCaptchaInstrument.send_post_request(
171
+ payload={"clientKey": API_KEY_STUB},
172
+ url_postfix=EndpointPostfixEnm.GET_BALANCE,
173
+ )
174
+
175
+ assert result == {"errorId": 0, "balance": 107.7}
176
+ assert mock_post.call_args.args[0] == f"{REQUEST_URL}/{EndpointPostfixEnm.GET_BALANCE.value}"
177
+ assert mock_post.call_args.kwargs["json"] == {"clientKey": API_KEY_STUB}
178
+
179
+ @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post")
180
+ async def test_send_post_request_raises_for_error_status(self, mock_post):
181
+ mock_post.return_value = aio_response(status=403)
182
+
183
+ with pytest.raises(ValueError):
184
+ await AIOCaptchaInstrument.send_post_request(payload={"clientKey": API_KEY_STUB})
185
+
186
+ mock_post.assert_called_once()
@@ -1,3 +1,5 @@
1
+ from unittest.mock import MagicMock, patch
2
+
1
3
  import pytest
2
4
 
3
5
  from tests.conftest import BaseTest
@@ -11,8 +13,8 @@ class TestFileInstrument(BaseTest):
11
13
  """
12
14
 
13
15
  def test_file_processing_exist(self):
14
- assert "file_processing" in FileInstrument.__dict__.keys()
15
- assert "aio_file_processing" in FileInstrument.__dict__.keys()
16
+ assert "file_processing" in FileInstrument.__dict__
17
+ assert "aio_file_processing" in FileInstrument.__dict__
16
18
 
17
19
  def test_captcha_base64(self):
18
20
  assert self.read_image_as_str() == FileInstrument().file_processing(captcha_base64=self.read_image())
@@ -55,3 +57,15 @@ class TestFileInstrument(BaseTest):
55
57
  async def test_aio_file_processing_err(self):
56
58
  with pytest.raises(ValueError):
57
59
  await FileInstrument().aio_file_processing()
60
+
61
+ @patch("python3_capsolver.core.captcha_instrument.aiohttp.ClientSession.get")
62
+ async def test_aio_captcha_link_download_err(self, mock_get):
63
+ mock_response = MagicMock()
64
+ mock_response.status = 404
65
+ mock_response.__aenter__.return_value = mock_response
66
+ mock_get.return_value = mock_response
67
+
68
+ result = await FileInstrument().aio_file_processing(captcha_link=self.image_captcha_url_example)
69
+
70
+ # unreachable link returns empty content instead of raising
71
+ assert result == ""
@@ -0,0 +1,185 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+
5
+ from tests.conftest import BaseTest
6
+ from python3_capsolver.core.base import CaptchaParams
7
+ from python3_capsolver.core.enum import CaptchaTypeEnm, EndpointPostfixEnm
8
+ from python3_capsolver.core.const import REQUEST_URL
9
+ from python3_capsolver.core.sio_captcha_instrument import SIOCaptchaInstrument
10
+
11
+ API_KEY_STUB = "unit-test-api-key-000000000000000000000000"
12
+ CREATE_TASK_URL = "https://unit.test/createTask"
13
+ GET_TASK_RESULT_URL = "https://unit.test/getTaskResult"
14
+
15
+
16
+ def create_captcha_params() -> CaptchaParams:
17
+ """
18
+ Build params pointing to a fake API host with zero sleep between polls:
19
+ no real request can leave the mocked `Session.post` boundary
20
+ """
21
+ return CaptchaParams(
22
+ api_key=API_KEY_STUB,
23
+ captcha_type=CaptchaTypeEnm.Control,
24
+ sleep_time=0,
25
+ request_url="https://unit.test/",
26
+ )
27
+
28
+
29
+ def sio_response(payload=None, status_code: int = 200) -> MagicMock:
30
+ """
31
+ Build fake response for the `requests` boundary.
32
+
33
+ For non-parsable bodies (``payload is None``) any ``json()`` call raises -
34
+ an error status must never be parsed as a success body
35
+ """
36
+ response = MagicMock()
37
+ response.status_code = status_code
38
+ if payload is None:
39
+ response.json.side_effect = AssertionError("body of an error response must not be parsed")
40
+ else:
41
+ response.json.return_value = payload
42
+ response.raise_for_status.return_value = None
43
+ return response
44
+
45
+
46
+ class TestSIOCaptchaInstrument(BaseTest):
47
+ """
48
+ Mocked unit tests for SIOCaptchaInstrument
49
+ """
50
+
51
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
52
+ def test_returns_created_result_without_polling_when_task_is_ready(self, mock_post):
53
+ mock_post.return_value = sio_response(
54
+ {"errorId": 0, "taskId": "task-ready-1", "status": "ready", "solution": {"text": "wg6x"}}
55
+ )
56
+
57
+ result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
58
+
59
+ assert result["errorId"] == 0
60
+ assert result["status"] == "ready"
61
+ assert result["taskId"] == "task-ready-1"
62
+ assert result["solution"] == {"text": "wg6x"}
63
+ # task was solved at creation - no `getTaskResult` polling must follow
64
+ mock_post.assert_called_once()
65
+ assert mock_post.call_args.args[0] == CREATE_TASK_URL
66
+ assert mock_post.call_args.kwargs["json"]["task"] == {"type": "Control"}
67
+ assert mock_post.call_args.kwargs["json"]["clientKey"] == API_KEY_STUB
68
+
69
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
70
+ def test_polls_get_task_result_until_ready(self, mock_post):
71
+ mock_post.side_effect = [
72
+ sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
73
+ sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
74
+ sio_response({"errorId": 0, "taskId": "task-42", "status": "ready", "solution": {"text": "ab12"}}),
75
+ ]
76
+
77
+ result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
78
+
79
+ assert result["errorId"] == 0
80
+ assert result["status"] == "ready"
81
+ assert result["solution"] == {"text": "ab12"}
82
+ assert [call.args[0] for call in mock_post.call_args_list] == [
83
+ CREATE_TASK_URL,
84
+ GET_TASK_RESULT_URL,
85
+ GET_TASK_RESULT_URL,
86
+ ]
87
+ # created taskId must be propagated to every polling request
88
+ assert mock_post.call_args_list[1].kwargs["json"]["taskId"] == "task-42"
89
+ assert mock_post.call_args_list[2].kwargs["json"]["taskId"] == "task-42"
90
+
91
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
92
+ def test_returns_failed_result_when_polling_reports_failure(self, mock_post):
93
+ mock_post.side_effect = [
94
+ sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
95
+ sio_response({"errorId": 1, "errorCode": "ERROR_TASK_NOT_FOUND", "status": "failed"}),
96
+ ]
97
+
98
+ result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
99
+
100
+ assert result["errorId"] == 1
101
+ assert result["status"] == "failed"
102
+ assert result["errorCode"] == "ERROR_TASK_NOT_FOUND"
103
+ assert len(mock_post.call_args_list) == 2
104
+
105
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
106
+ def test_marks_result_failed_when_task_creation_is_rejected(self, mock_post):
107
+ mock_post.return_value = sio_response(
108
+ {"errorId": 1, "errorCode": "ERROR_KEY_DOES_NOT_EXIST", "errorDescription": "Client key is invalid"}
109
+ )
110
+
111
+ result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
112
+
113
+ assert result["errorId"] == 1
114
+ assert result["status"] == "failed"
115
+ assert result["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST"
116
+ assert result["errorDescription"] == "Client key is invalid"
117
+ # rejected creation must not trigger result polling
118
+ mock_post.assert_called_once()
119
+
120
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
121
+ def test_parses_error_payload_when_status_code_is_valid_error_status(self, mock_post):
122
+ mock_post.return_value = sio_response({"errorId": 1, "errorCode": "ERROR_WRONG_USERKEY"}, status_code=400)
123
+
124
+ result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
125
+
126
+ assert result["status"] == "failed"
127
+ assert result["errorCode"] == "ERROR_WRONG_USERKEY"
128
+
129
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
130
+ def test_returns_unsolvable_default_when_server_never_finishes(self, mock_post):
131
+ mock_post.side_effect = [sio_response({"errorId": 0, "taskId": "task-silent", "status": "processing"})] * 16
132
+
133
+ result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
134
+
135
+ assert result["errorId"] == 1
136
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
137
+ assert result["errorDescription"] == "Captcha not recognized"
138
+ assert result["status"] == "failed"
139
+ assert result["taskId"] == "task-silent"
140
+ # 1 `createTask` + 15 polling attempts, then the instrument gives up
141
+ assert len(mock_post.call_args_list) == 16
142
+
143
+ @pytest.mark.parametrize("bad_status", [404, 500])
144
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
145
+ def test_raises_when_create_task_returns_invalid_status(self, mock_post, bad_status):
146
+ mock_post.return_value = sio_response(status_code=bad_status)
147
+
148
+ with pytest.raises(ValueError):
149
+ SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
150
+
151
+ mock_post.assert_called_once()
152
+
153
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
154
+ def test_raises_when_get_task_result_returns_invalid_status(self, mock_post):
155
+ mock_post.side_effect = [
156
+ sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}),
157
+ sio_response(status_code=503),
158
+ ]
159
+
160
+ with pytest.raises(ValueError):
161
+ SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha()
162
+
163
+ assert len(mock_post.call_args_list) == 2
164
+
165
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
166
+ def test_send_post_request_returns_decoded_payload(self, mock_post):
167
+ mock_post.return_value = sio_response({"errorId": 0, "balance": 107.7})
168
+
169
+ result = SIOCaptchaInstrument.send_post_request(
170
+ payload={"clientKey": API_KEY_STUB},
171
+ url_postfix=EndpointPostfixEnm.GET_BALANCE,
172
+ )
173
+
174
+ assert result == {"errorId": 0, "balance": 107.7}
175
+ assert mock_post.call_args.args[0] == f"{REQUEST_URL}/{EndpointPostfixEnm.GET_BALANCE.value}"
176
+ assert mock_post.call_args.kwargs["json"] == {"clientKey": API_KEY_STUB}
177
+
178
+ @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post")
179
+ def test_send_post_request_raises_for_error_status(self, mock_post):
180
+ mock_post.return_value = sio_response(status_code=403)
181
+
182
+ with pytest.raises(ValueError):
183
+ SIOCaptchaInstrument.send_post_request(payload={"clientKey": API_KEY_STUB})
184
+
185
+ mock_post.assert_called_once()
@@ -1 +0,0 @@
1
- __version__ = "1.2.0"