flaresolverr-cli 0.2.1__tar.gz → 0.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.
- {flaresolverr_cli-0.2.1/flaresolverr_cli.egg-info → flaresolverr_cli-0.2.2}/PKG-INFO +11 -7
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/README.md +10 -6
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2/flaresolverr_cli.egg-info}/PKG-INFO +11 -7
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/flaresolverr_cli.egg-info/SOURCES.txt +5 -3
- flaresolverr_cli-0.2.2/flaresolverr_cli.egg-info/entry_points.txt +2 -0
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/flaresolverr_cli.egg-info/top_level.txt +0 -2
- flaresolverr_cli-0.2.2/flaresolverr_session/__init__.py +28 -0
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2/flaresolverr_session}/cli.py +13 -9
- flaresolverr_cli-0.2.2/flaresolverr_session/exceptions.py +36 -0
- flaresolverr_cli-0.2.1/flaresolverr_rpc.py → flaresolverr_cli-0.2.2/flaresolverr_session/rpc.py +66 -45
- flaresolverr_cli-0.2.1/flaresolverr_session.py → flaresolverr_cli-0.2.2/flaresolverr_session/session.py +67 -120
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/setup.py +7 -6
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/tests/test_cli.py +74 -26
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/tests/test_rpc.py +1 -14
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/tests/test_session.py +17 -28
- flaresolverr_cli-0.2.1/flaresolverr_cli.egg-info/entry_points.txt +0 -2
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/LICENSE +0 -0
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/flaresolverr_cli.egg-info/dependency_links.txt +0 -0
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/flaresolverr_cli.egg-info/requires.txt +0 -0
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/setup.cfg +0 -0
- {flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/tests/testconf.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flaresolverr-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A requests.Session that proxies through a FlareSolverr instance.
|
|
5
5
|
Home-page: https://github.com/Xavier-Lam/FlareSolverrSession
|
|
6
6
|
Author: Xavier-Lam
|
|
@@ -114,16 +114,14 @@ All exceptions defined in the module based on `FlareSolverrError`, which inherit
|
|
|
114
114
|
requests.RequestException
|
|
115
115
|
└── FlareSolverrError
|
|
116
116
|
├── FlareSolverrResponseError
|
|
117
|
-
│ ├──
|
|
118
|
-
│ └── FlareSolverrTimeoutError
|
|
117
|
+
│ ├── FlareSolverrChallengeError
|
|
119
118
|
└── FlareSolverrUnsupportedMethodError
|
|
120
119
|
|
|
121
120
|
|
|
122
121
|
| Exception | Description |
|
|
123
122
|
|---|---|
|
|
124
123
|
| `FlareSolverrResponseError` | FlareSolverr returned an error response. The response dict is available as `response_data` attribute. |
|
|
125
|
-
| `
|
|
126
|
-
| `FlareSolverrTimeoutError` | Request timed out. |
|
|
124
|
+
| `FlareSolverrChallengeError` | Challenge solving failed. |
|
|
127
125
|
| `FlareSolverrUnsupportedMethodError` | Unsupported HTTP method or content type. |
|
|
128
126
|
|
|
129
127
|
### Command-Line Interface
|
|
@@ -149,14 +147,20 @@ flaresolverr-cli https://example.com -d "key=value&foo=bar"
|
|
|
149
147
|
#### Managing sessions
|
|
150
148
|
|
|
151
149
|
```bash
|
|
152
|
-
# Create a session
|
|
150
|
+
# Create a session
|
|
153
151
|
flaresolverr-cli -f http://localhost:8191/v1 session create my-session
|
|
154
152
|
|
|
153
|
+
# Create multiple sessions at once
|
|
154
|
+
flaresolverr-cli session create session1 session2 session3
|
|
155
|
+
|
|
155
156
|
# List all active sessions
|
|
156
157
|
flaresolverr-cli session list
|
|
157
158
|
|
|
158
159
|
# Destroy a session
|
|
159
160
|
flaresolverr-cli session destroy my-session
|
|
161
|
+
|
|
162
|
+
# Clear all sessions
|
|
163
|
+
flaresolverr-cli session clear
|
|
160
164
|
```
|
|
161
165
|
|
|
162
166
|
### RPC Tool
|
|
@@ -164,7 +168,7 @@ flaresolverr-cli session destroy my-session
|
|
|
164
168
|
The `flaresolverr_rpc` module provides a programmatic interface to the FlareSolverr JSON API, useful when you need low-level access to the raw API responses.
|
|
165
169
|
|
|
166
170
|
```python
|
|
167
|
-
from
|
|
171
|
+
from flaresolverr_session import RPC
|
|
168
172
|
|
|
169
173
|
with RPC("http://localhost:8191/v1") as rpc:
|
|
170
174
|
# Session management
|
|
@@ -70,16 +70,14 @@ All exceptions defined in the module based on `FlareSolverrError`, which inherit
|
|
|
70
70
|
requests.RequestException
|
|
71
71
|
└── FlareSolverrError
|
|
72
72
|
├── FlareSolverrResponseError
|
|
73
|
-
│ ├──
|
|
74
|
-
│ └── FlareSolverrTimeoutError
|
|
73
|
+
│ ├── FlareSolverrChallengeError
|
|
75
74
|
└── FlareSolverrUnsupportedMethodError
|
|
76
75
|
|
|
77
76
|
|
|
78
77
|
| Exception | Description |
|
|
79
78
|
|---|---|
|
|
80
79
|
| `FlareSolverrResponseError` | FlareSolverr returned an error response. The response dict is available as `response_data` attribute. |
|
|
81
|
-
| `
|
|
82
|
-
| `FlareSolverrTimeoutError` | Request timed out. |
|
|
80
|
+
| `FlareSolverrChallengeError` | Challenge solving failed. |
|
|
83
81
|
| `FlareSolverrUnsupportedMethodError` | Unsupported HTTP method or content type. |
|
|
84
82
|
|
|
85
83
|
### Command-Line Interface
|
|
@@ -105,14 +103,20 @@ flaresolverr-cli https://example.com -d "key=value&foo=bar"
|
|
|
105
103
|
#### Managing sessions
|
|
106
104
|
|
|
107
105
|
```bash
|
|
108
|
-
# Create a session
|
|
106
|
+
# Create a session
|
|
109
107
|
flaresolverr-cli -f http://localhost:8191/v1 session create my-session
|
|
110
108
|
|
|
109
|
+
# Create multiple sessions at once
|
|
110
|
+
flaresolverr-cli session create session1 session2 session3
|
|
111
|
+
|
|
111
112
|
# List all active sessions
|
|
112
113
|
flaresolverr-cli session list
|
|
113
114
|
|
|
114
115
|
# Destroy a session
|
|
115
116
|
flaresolverr-cli session destroy my-session
|
|
117
|
+
|
|
118
|
+
# Clear all sessions
|
|
119
|
+
flaresolverr-cli session clear
|
|
116
120
|
```
|
|
117
121
|
|
|
118
122
|
### RPC Tool
|
|
@@ -120,7 +124,7 @@ flaresolverr-cli session destroy my-session
|
|
|
120
124
|
The `flaresolverr_rpc` module provides a programmatic interface to the FlareSolverr JSON API, useful when you need low-level access to the raw API responses.
|
|
121
125
|
|
|
122
126
|
```python
|
|
123
|
-
from
|
|
127
|
+
from flaresolverr_session import RPC
|
|
124
128
|
|
|
125
129
|
with RPC("http://localhost:8191/v1") as rpc:
|
|
126
130
|
# Session management
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flaresolverr-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A requests.Session that proxies through a FlareSolverr instance.
|
|
5
5
|
Home-page: https://github.com/Xavier-Lam/FlareSolverrSession
|
|
6
6
|
Author: Xavier-Lam
|
|
@@ -114,16 +114,14 @@ All exceptions defined in the module based on `FlareSolverrError`, which inherit
|
|
|
114
114
|
requests.RequestException
|
|
115
115
|
└── FlareSolverrError
|
|
116
116
|
├── FlareSolverrResponseError
|
|
117
|
-
│ ├──
|
|
118
|
-
│ └── FlareSolverrTimeoutError
|
|
117
|
+
│ ├── FlareSolverrChallengeError
|
|
119
118
|
└── FlareSolverrUnsupportedMethodError
|
|
120
119
|
|
|
121
120
|
|
|
122
121
|
| Exception | Description |
|
|
123
122
|
|---|---|
|
|
124
123
|
| `FlareSolverrResponseError` | FlareSolverr returned an error response. The response dict is available as `response_data` attribute. |
|
|
125
|
-
| `
|
|
126
|
-
| `FlareSolverrTimeoutError` | Request timed out. |
|
|
124
|
+
| `FlareSolverrChallengeError` | Challenge solving failed. |
|
|
127
125
|
| `FlareSolverrUnsupportedMethodError` | Unsupported HTTP method or content type. |
|
|
128
126
|
|
|
129
127
|
### Command-Line Interface
|
|
@@ -149,14 +147,20 @@ flaresolverr-cli https://example.com -d "key=value&foo=bar"
|
|
|
149
147
|
#### Managing sessions
|
|
150
148
|
|
|
151
149
|
```bash
|
|
152
|
-
# Create a session
|
|
150
|
+
# Create a session
|
|
153
151
|
flaresolverr-cli -f http://localhost:8191/v1 session create my-session
|
|
154
152
|
|
|
153
|
+
# Create multiple sessions at once
|
|
154
|
+
flaresolverr-cli session create session1 session2 session3
|
|
155
|
+
|
|
155
156
|
# List all active sessions
|
|
156
157
|
flaresolverr-cli session list
|
|
157
158
|
|
|
158
159
|
# Destroy a session
|
|
159
160
|
flaresolverr-cli session destroy my-session
|
|
161
|
+
|
|
162
|
+
# Clear all sessions
|
|
163
|
+
flaresolverr-cli session clear
|
|
160
164
|
```
|
|
161
165
|
|
|
162
166
|
### RPC Tool
|
|
@@ -164,7 +168,7 @@ flaresolverr-cli session destroy my-session
|
|
|
164
168
|
The `flaresolverr_rpc` module provides a programmatic interface to the FlareSolverr JSON API, useful when you need low-level access to the raw API responses.
|
|
165
169
|
|
|
166
170
|
```python
|
|
167
|
-
from
|
|
171
|
+
from flaresolverr_session import RPC
|
|
168
172
|
|
|
169
173
|
with RPC("http://localhost:8191/v1") as rpc:
|
|
170
174
|
# Session management
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
|
-
cli.py
|
|
4
|
-
flaresolverr_rpc.py
|
|
5
|
-
flaresolverr_session.py
|
|
6
3
|
setup.py
|
|
7
4
|
flaresolverr_cli.egg-info/PKG-INFO
|
|
8
5
|
flaresolverr_cli.egg-info/SOURCES.txt
|
|
@@ -10,6 +7,11 @@ flaresolverr_cli.egg-info/dependency_links.txt
|
|
|
10
7
|
flaresolverr_cli.egg-info/entry_points.txt
|
|
11
8
|
flaresolverr_cli.egg-info/requires.txt
|
|
12
9
|
flaresolverr_cli.egg-info/top_level.txt
|
|
10
|
+
flaresolverr_session/__init__.py
|
|
11
|
+
flaresolverr_session/cli.py
|
|
12
|
+
flaresolverr_session/exceptions.py
|
|
13
|
+
flaresolverr_session/rpc.py
|
|
14
|
+
flaresolverr_session/session.py
|
|
13
15
|
tests/test_cli.py
|
|
14
16
|
tests/test_rpc.py
|
|
15
17
|
tests/test_session.py
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from flaresolverr_session.exceptions import (
|
|
4
|
+
FlareSolverrError,
|
|
5
|
+
FlareSolverrResponseError,
|
|
6
|
+
FlareSolverrChallengeError,
|
|
7
|
+
FlareSolverrUnsupportedMethodError,
|
|
8
|
+
)
|
|
9
|
+
from flaresolverr_session.rpc import RPC
|
|
10
|
+
from flaresolverr_session.session import Session, Response
|
|
11
|
+
|
|
12
|
+
__title__ = "flaresolverr-session"
|
|
13
|
+
__description__ = "A requests.Session that proxies through a FlareSolverr instance."
|
|
14
|
+
__url__ = "https://github.com/Xavier-Lam/FlareSolverrSession"
|
|
15
|
+
__version__ = "0.2.2"
|
|
16
|
+
__author__ = "Xavier-Lam"
|
|
17
|
+
__author_email__ = "xavierlam7@hotmail.com"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Session",
|
|
21
|
+
"Response",
|
|
22
|
+
"RPC",
|
|
23
|
+
"FlareSolverrError",
|
|
24
|
+
"FlareSolverrResponseError",
|
|
25
|
+
"FlareSolverrChallengeError",
|
|
26
|
+
"FlareSolverrUnsupportedMethodError",
|
|
27
|
+
"__version__",
|
|
28
|
+
]
|
|
@@ -9,8 +9,8 @@ import json
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
11
|
|
|
12
|
-
from
|
|
13
|
-
from flaresolverr_session import FlareSolverrResponseError
|
|
12
|
+
from flaresolverr_session.rpc import RPC
|
|
13
|
+
from flaresolverr_session.exceptions import FlareSolverrResponseError
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def main(argv=None):
|
|
@@ -101,9 +101,8 @@ def _build_session_parser():
|
|
|
101
101
|
create_parser = session_sub.add_parser("create", help="Create a session")
|
|
102
102
|
create_parser.add_argument(
|
|
103
103
|
"name",
|
|
104
|
-
nargs="
|
|
105
|
-
|
|
106
|
-
help="Optional session name",
|
|
104
|
+
nargs="+",
|
|
105
|
+
help="One or more session names to create",
|
|
107
106
|
)
|
|
108
107
|
create_parser.add_argument(
|
|
109
108
|
"--proxy",
|
|
@@ -121,6 +120,9 @@ def _build_session_parser():
|
|
|
121
120
|
help="Session identifier to destroy",
|
|
122
121
|
)
|
|
123
122
|
|
|
123
|
+
# session clear
|
|
124
|
+
session_sub.add_parser("clear", help="Destroy all sessions")
|
|
125
|
+
|
|
124
126
|
return parser
|
|
125
127
|
|
|
126
128
|
|
|
@@ -213,14 +215,16 @@ def _handle_session(rpc, args):
|
|
|
213
215
|
action = args.session_action
|
|
214
216
|
|
|
215
217
|
if action == "create":
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
)
|
|
218
|
+
names = getattr(args, "name", None)
|
|
219
|
+
proxy = getattr(args, "proxy", None)
|
|
220
|
+
return [rpc.session.create(session_id=n, proxy=proxy) for n in names]
|
|
220
221
|
elif action == "list":
|
|
221
222
|
return rpc.session.list()
|
|
222
223
|
elif action == "destroy":
|
|
223
224
|
return rpc.session.destroy(args.session_id)
|
|
225
|
+
elif action == "clear":
|
|
226
|
+
payload = rpc.session.list()
|
|
227
|
+
return [rpc.session.destroy(s) for s in payload["sessions"]]
|
|
224
228
|
else:
|
|
225
229
|
raise ValueError("Unknown session action: %s" % action)
|
|
226
230
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"FlareSolverrError",
|
|
7
|
+
"FlareSolverrResponseError",
|
|
8
|
+
"FlareSolverrChallengeError",
|
|
9
|
+
"FlareSolverrUnsupportedMethodError",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FlareSolverrError(requests.RequestException):
|
|
14
|
+
"""Base exception for FlareSolverr errors."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlareSolverrResponseError(FlareSolverrError):
|
|
18
|
+
"""Raised when FlareSolverr returns a non-ok response.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
message (str): The error message from FlareSolverr.
|
|
22
|
+
response_data (dict or None): The original FlareSolverr response dict.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message, response_data=None, **kwargs):
|
|
26
|
+
super(FlareSolverrResponseError, self).__init__(message, **kwargs)
|
|
27
|
+
self.message = message or ""
|
|
28
|
+
self.response_data = response_data
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FlareSolverrChallengeError(FlareSolverrResponseError):
|
|
32
|
+
"""Raised when a challenge could not be solved."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FlareSolverrUnsupportedMethodError(FlareSolverrError):
|
|
36
|
+
"""Raised when an unsupported HTTP method or content type is used."""
|
flaresolverr_cli-0.2.1/flaresolverr_rpc.py → flaresolverr_cli-0.2.2/flaresolverr_session/rpc.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
from __future__ import print_function
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
|
|
6
7
|
try:
|
|
@@ -10,10 +11,69 @@ except ImportError:
|
|
|
10
11
|
|
|
11
12
|
import requests
|
|
12
13
|
|
|
13
|
-
from flaresolverr_session import
|
|
14
|
+
from flaresolverr_session.exceptions import FlareSolverrError, FlareSolverrResponseError
|
|
14
15
|
|
|
16
|
+
__all__ = [
|
|
17
|
+
"RPC",
|
|
18
|
+
"SessionCommand",
|
|
19
|
+
"RequestCommand",
|
|
20
|
+
"FlareSolverrError",
|
|
21
|
+
"FlareSolverrResponseError",
|
|
22
|
+
]
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
|
|
25
|
+
class RPC(object):
|
|
26
|
+
"""RPC client for FlareSolverr.
|
|
27
|
+
|
|
28
|
+
Provides two sub-command namespaces:
|
|
29
|
+
|
|
30
|
+
- :attr:`session` -- :class:`SessionCommand` for session management.
|
|
31
|
+
- :attr:`request` -- :class:`RequestCommand` for HTTP requests.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
flaresolverr_url (str): The FlareSolverr API endpoint
|
|
35
|
+
(e.g. ``"http://localhost:8191/v1"``).
|
|
36
|
+
api_session (requests.Session or None): An optional pre-configured
|
|
37
|
+
session to use for API calls.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, flaresolverr_url=None, api_session=None):
|
|
41
|
+
if flaresolverr_url is None:
|
|
42
|
+
flaresolverr_url = os.environ.get(
|
|
43
|
+
"FLARESOLVERR_URL", "http://localhost:8191/v1"
|
|
44
|
+
)
|
|
45
|
+
if api_session is None:
|
|
46
|
+
api_session = requests.Session()
|
|
47
|
+
|
|
48
|
+
self._flaresolverr_url = flaresolverr_url
|
|
49
|
+
self._api_session = api_session
|
|
50
|
+
self.session = SessionCommand(self)
|
|
51
|
+
self.request = RequestCommand(self)
|
|
52
|
+
|
|
53
|
+
def send(self, payload):
|
|
54
|
+
"""Send a JSON payload to the FlareSolverr endpoint.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
payload (dict): The JSON-serialisable payload to send.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
dict: The parsed JSON response from FlareSolverr.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
FlareSolverrResponseError: If the response status is
|
|
64
|
+
not ``"ok"``.
|
|
65
|
+
"""
|
|
66
|
+
headers = {"Content-Type": "application/json"}
|
|
67
|
+
resp = self._api_session.post(
|
|
68
|
+
self._flaresolverr_url,
|
|
69
|
+
headers=headers,
|
|
70
|
+
data=json.dumps(payload),
|
|
71
|
+
)
|
|
72
|
+
data = resp.json()
|
|
73
|
+
status = data.get("status", "")
|
|
74
|
+
if status != "ok":
|
|
75
|
+
raise FlareSolverrResponseError(data.get("message"), data, response=resp)
|
|
76
|
+
return data
|
|
17
77
|
|
|
18
78
|
|
|
19
79
|
class SessionCommand(object):
|
|
@@ -59,7 +119,7 @@ class SessionCommand(object):
|
|
|
59
119
|
payload["proxy"] = proxy
|
|
60
120
|
else:
|
|
61
121
|
payload["proxy"] = {"url": proxy}
|
|
62
|
-
return self._rpc.
|
|
122
|
+
return self._rpc.send(payload)
|
|
63
123
|
|
|
64
124
|
def list(self):
|
|
65
125
|
"""List all active FlareSolverr browser sessions.
|
|
@@ -78,7 +138,7 @@ class SessionCommand(object):
|
|
|
78
138
|
timestamp (ms).
|
|
79
139
|
"""
|
|
80
140
|
payload = {"cmd": "sessions.list"}
|
|
81
|
-
return self._rpc.
|
|
141
|
+
return self._rpc.send(payload)
|
|
82
142
|
|
|
83
143
|
def destroy(self, session_id):
|
|
84
144
|
"""Destroy an existing FlareSolverr browser session.
|
|
@@ -102,7 +162,7 @@ class SessionCommand(object):
|
|
|
102
162
|
"cmd": "sessions.destroy",
|
|
103
163
|
"session": session_id,
|
|
104
164
|
}
|
|
105
|
-
return self._rpc.
|
|
165
|
+
return self._rpc.send(payload)
|
|
106
166
|
|
|
107
167
|
|
|
108
168
|
class RequestCommand(object):
|
|
@@ -344,43 +404,4 @@ class RequestCommand(object):
|
|
|
344
404
|
elif cmd == "request.post":
|
|
345
405
|
# FlareSolverr requires postData for request.post; send empty string
|
|
346
406
|
payload["postData"] = ""
|
|
347
|
-
return self._rpc.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
class RPC(object):
|
|
351
|
-
"""RPC client for FlareSolverr.
|
|
352
|
-
|
|
353
|
-
Provides two sub-command namespaces:
|
|
354
|
-
|
|
355
|
-
- :attr:`session` -- :class:`SessionCommand` for session management.
|
|
356
|
-
- :attr:`request` -- :class:`RequestCommand` for HTTP requests.
|
|
357
|
-
|
|
358
|
-
The underlying transport reuses
|
|
359
|
-
:meth:`flaresolverr_session.Session.send`, which is the same
|
|
360
|
-
low-level method used by the high-level
|
|
361
|
-
:class:`~flaresolverr_session.Session`.
|
|
362
|
-
|
|
363
|
-
Parameters:
|
|
364
|
-
flaresolverr_url (str): The FlareSolverr API endpoint
|
|
365
|
-
(e.g. ``"http://localhost:8191/v1"``).
|
|
366
|
-
api_session (requests.Session or None): An optional pre-configured
|
|
367
|
-
session to use for API calls.
|
|
368
|
-
"""
|
|
369
|
-
|
|
370
|
-
def __init__(self, flaresolverr_url=None, api_session=None):
|
|
371
|
-
if flaresolverr_url is None:
|
|
372
|
-
flaresolverr_url = os.environ.get(
|
|
373
|
-
"FLARESOLVERR_URL", "http://localhost:8191/v1"
|
|
374
|
-
)
|
|
375
|
-
if api_session is None:
|
|
376
|
-
api_session = requests.Session()
|
|
377
|
-
|
|
378
|
-
self._session = Session(flaresolverr_url, session=api_session)
|
|
379
|
-
self.session = SessionCommand(self)
|
|
380
|
-
self.request = RequestCommand(self)
|
|
381
|
-
|
|
382
|
-
def __enter__(self):
|
|
383
|
-
return self
|
|
384
|
-
|
|
385
|
-
def __exit__(self, *args):
|
|
386
|
-
pass
|
|
407
|
+
return self._rpc.send(payload)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import warnings
|
|
4
4
|
|
|
5
5
|
try:
|
|
6
6
|
from urllib import urlencode
|
|
@@ -10,47 +10,12 @@ except ImportError:
|
|
|
10
10
|
import requests
|
|
11
11
|
from requests.structures import CaseInsensitiveDict
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
__all__ = ["Session", "FlareSolverr", "Response", "FlareSolverrError",
|
|
21
|
-
"FlareSolverrResponseError", "FlareSolverrCaptchaError",
|
|
22
|
-
"FlareSolverrTimeoutError", "FlareSolverrUnsupportedMethodError",
|
|
23
|
-
"__version__",]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class FlareSolverrError(requests.RequestException):
|
|
27
|
-
"""Base exception for FlareSolverr errors."""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class FlareSolverrResponseError(FlareSolverrError):
|
|
31
|
-
"""Raised when FlareSolverr returns a non-ok response.
|
|
32
|
-
|
|
33
|
-
Attributes:
|
|
34
|
-
message (str): The error message from FlareSolverr.
|
|
35
|
-
response_data (dict or None): The original FlareSolverr response dict.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
def __init__(self, message, response_data=None, **kwargs):
|
|
39
|
-
super(FlareSolverrResponseError, self).__init__(message, **kwargs)
|
|
40
|
-
self.message = message or ""
|
|
41
|
-
self.response_data = response_data
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class FlareSolverrCaptchaError(FlareSolverrResponseError):
|
|
45
|
-
"""Raised when a CAPTCHA was encountered but could not be solved."""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class FlareSolverrTimeoutError(FlareSolverrResponseError):
|
|
49
|
-
"""Raised when FlareSolverr timed out while solving the challenge."""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class FlareSolverrUnsupportedMethodError(FlareSolverrError):
|
|
53
|
-
"""Raised when an unsupported HTTP method or content type is used."""
|
|
13
|
+
from flaresolverr_session.rpc import RPC
|
|
14
|
+
from flaresolverr_session.exceptions import (
|
|
15
|
+
FlareSolverrResponseError,
|
|
16
|
+
FlareSolverrChallengeError,
|
|
17
|
+
FlareSolverrUnsupportedMethodError,
|
|
18
|
+
)
|
|
54
19
|
|
|
55
20
|
|
|
56
21
|
class Session(requests.Session):
|
|
@@ -62,8 +27,9 @@ class Session(requests.Session):
|
|
|
62
27
|
FlareSolverr-specific attributes.
|
|
63
28
|
|
|
64
29
|
Parameters:
|
|
65
|
-
flaresolverr_url (str): The FlareSolverr API endpoint
|
|
66
|
-
(e.g. ``"http://localhost:8191/v1"``).
|
|
30
|
+
flaresolverr_url (str or None): The FlareSolverr API endpoint
|
|
31
|
+
(e.g. ``"http://localhost:8191/v1"``). Ignored when *rpc*
|
|
32
|
+
is provided.
|
|
67
33
|
session_id (str or None): An optional FlareSolverr session id.
|
|
68
34
|
When *None* a new session is automatically created.
|
|
69
35
|
proxy (str, dict or None): A proxy specification.
|
|
@@ -72,8 +38,9 @@ class Session(requests.Session):
|
|
|
72
38
|
``"username"`` and ``"password"`` keys.
|
|
73
39
|
timeout (int or None): ``maxTimeout`` in **milliseconds**
|
|
74
40
|
passed to FlareSolverr. Defaults to *60000* (60 s).
|
|
75
|
-
|
|
76
|
-
|
|
41
|
+
rpc (RPC or None): An optional pre-configured
|
|
42
|
+
:class:`~flaresolverr_session.rpc.RPC` instance. When provided,
|
|
43
|
+
*flaresolverr_url* is ignored.
|
|
77
44
|
|
|
78
45
|
.. note::
|
|
79
46
|
|
|
@@ -87,15 +54,28 @@ class Session(requests.Session):
|
|
|
87
54
|
|
|
88
55
|
_SUPPORTED_METHODS = ("GET", "POST")
|
|
89
56
|
|
|
90
|
-
def __init__(
|
|
91
|
-
|
|
57
|
+
def __init__(
|
|
58
|
+
self, flaresolverr_url=None, session_id=None, proxy=None, timeout=None, rpc=None
|
|
59
|
+
):
|
|
92
60
|
super(Session, self).__init__()
|
|
93
|
-
self._flaresolverr_url = flaresolverr_url
|
|
94
|
-
self._timeout = timeout or self.DEFAULT_TIMEOUT
|
|
95
|
-
self._proxy = self._normalise_proxy(proxy)
|
|
96
61
|
|
|
62
|
+
if rpc is not None and flaresolverr_url is not None:
|
|
63
|
+
warnings.warn(
|
|
64
|
+
"Both 'rpc' and 'flaresolverr_url' are provided. "
|
|
65
|
+
"The 'rpc' instance will be used and 'flaresolverr_url' "
|
|
66
|
+
"will be ignored.",
|
|
67
|
+
stacklevel=2,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not rpc:
|
|
71
|
+
rpc = RPC(flaresolverr_url)
|
|
72
|
+
self._rpc = rpc
|
|
73
|
+
|
|
74
|
+
self._timeout = timeout or self.DEFAULT_TIMEOUT
|
|
75
|
+
if proxy and not isinstance(proxy, dict):
|
|
76
|
+
proxy = {"url": proxy}
|
|
77
|
+
self._proxy = proxy
|
|
97
78
|
self._session_id = session_id
|
|
98
|
-
self._api_session = session or requests.Session()
|
|
99
79
|
self._session_created = False
|
|
100
80
|
|
|
101
81
|
@property
|
|
@@ -126,12 +106,9 @@ class Session(requests.Session):
|
|
|
126
106
|
FlareSolverrUnsupportedMethodError: If *method* is not
|
|
127
107
|
``GET`` or ``POST``, or if ``json`` data is passed.
|
|
128
108
|
FlareSolverrResponseError: If FlareSolverr returns a
|
|
129
|
-
non-ok status for
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
attribute.
|
|
133
|
-
FlareSolverrCaptchaError: If a CAPTCHA was detected.
|
|
134
|
-
FlareSolverrTimeoutError: If FlareSolverr timed out.
|
|
109
|
+
non-ok status for a reason not related to a challenge.
|
|
110
|
+
FlareSolverrChallengeError: If a challenge, CAPTCHA, or
|
|
111
|
+
timeout was encountered.
|
|
135
112
|
"""
|
|
136
113
|
method = method.upper()
|
|
137
114
|
if method not in self._SUPPORTED_METHODS:
|
|
@@ -146,28 +123,31 @@ class Session(requests.Session):
|
|
|
146
123
|
"POST requests. JSON POST is not supported."
|
|
147
124
|
)
|
|
148
125
|
|
|
149
|
-
|
|
126
|
+
request_kwargs = self._build_request_kwargs(method, url, **kwargs)
|
|
150
127
|
try:
|
|
151
|
-
|
|
128
|
+
if method == "GET":
|
|
129
|
+
resp_data = self._rpc.request.get(**request_kwargs)
|
|
130
|
+
else:
|
|
131
|
+
resp_data = self._rpc.request.post(**request_kwargs)
|
|
152
132
|
except FlareSolverrResponseError as e:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
133
|
+
msg = e.message.lower()
|
|
134
|
+
if "captcha" in msg or "timeout" in msg or "challenge" in msg:
|
|
135
|
+
raise FlareSolverrChallengeError(
|
|
136
|
+
e.message, response_data=e.response_data
|
|
137
|
+
)
|
|
157
138
|
raise
|
|
158
139
|
return Response(resp_data)
|
|
159
140
|
|
|
160
141
|
def close(self):
|
|
161
|
-
"""Destroy the FlareSolverr session and close
|
|
162
|
-
|
|
142
|
+
"""Destroy the FlareSolverr session and close the inherited
|
|
143
|
+
``requests.Session``."""
|
|
163
144
|
try:
|
|
164
145
|
if self._session_created:
|
|
165
146
|
self._destroy_session()
|
|
166
147
|
finally:
|
|
167
|
-
self._api_session.close()
|
|
168
148
|
super(Session, self).close()
|
|
169
149
|
|
|
170
|
-
def
|
|
150
|
+
def _build_request_kwargs(self, method, url, **kwargs):
|
|
171
151
|
params = kwargs.get("params")
|
|
172
152
|
if params:
|
|
173
153
|
if isinstance(params, dict):
|
|
@@ -177,12 +157,10 @@ class Session(requests.Session):
|
|
|
177
157
|
else:
|
|
178
158
|
url = url + "?" + encoded_params
|
|
179
159
|
|
|
180
|
-
|
|
181
|
-
payload = {
|
|
182
|
-
"cmd": cmd,
|
|
160
|
+
request_kwargs = {
|
|
183
161
|
"url": url,
|
|
184
|
-
"
|
|
185
|
-
"
|
|
162
|
+
"session_id": self.session_id,
|
|
163
|
+
"max_timeout": kwargs.get("timeout", self._timeout),
|
|
186
164
|
}
|
|
187
165
|
|
|
188
166
|
# POST data
|
|
@@ -199,64 +177,29 @@ class Session(requests.Session):
|
|
|
199
177
|
except NameError:
|
|
200
178
|
pass
|
|
201
179
|
data = str(data)
|
|
202
|
-
|
|
180
|
+
request_kwargs["data"] = data
|
|
203
181
|
|
|
204
182
|
# Optional cookies
|
|
205
183
|
cookies = kwargs.get("cookies")
|
|
206
184
|
if cookies:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return payload
|
|
210
|
-
|
|
211
|
-
@staticmethod
|
|
212
|
-
def _normalise_proxy(proxy):
|
|
213
|
-
if proxy is None:
|
|
214
|
-
return None
|
|
215
|
-
if isinstance(proxy, dict):
|
|
216
|
-
return proxy
|
|
217
|
-
return {"url": proxy}
|
|
218
|
-
|
|
219
|
-
def send(self, payload):
|
|
220
|
-
headers = {"Content-Type": "application/json"}
|
|
221
|
-
resp = self._api_session.post(
|
|
222
|
-
self._flaresolverr_url,
|
|
223
|
-
headers=headers,
|
|
224
|
-
data=json.dumps(payload),
|
|
225
|
-
)
|
|
226
|
-
data = resp.json()
|
|
227
|
-
status = data.get("status", "")
|
|
228
|
-
if status != "ok":
|
|
229
|
-
raise FlareSolverrResponseError(
|
|
230
|
-
data.get("message"), data, response=resp)
|
|
231
|
-
return data
|
|
232
|
-
|
|
233
|
-
def _create_session(self):
|
|
234
|
-
payload = {
|
|
235
|
-
"cmd": "sessions.create",
|
|
236
|
-
}
|
|
237
|
-
if self._session_id:
|
|
238
|
-
payload["session"] = self._session_id
|
|
239
|
-
if self._proxy:
|
|
240
|
-
payload["proxy"] = self._proxy
|
|
185
|
+
request_kwargs["cookies"] = cookies
|
|
241
186
|
|
|
242
|
-
|
|
187
|
+
return request_kwargs
|
|
243
188
|
|
|
189
|
+
def _create_session(self):
|
|
190
|
+
"""Create a FlareSolverr browser session via RPC."""
|
|
191
|
+
data = self._rpc.session.create(session_id=self._session_id, proxy=self._proxy)
|
|
244
192
|
self._session_id = data.get("session", self._session_id)
|
|
245
193
|
self._session_created = True
|
|
246
194
|
|
|
247
195
|
def _destroy_session(self):
|
|
196
|
+
"""Destroy the FlareSolverr browser session via RPC."""
|
|
248
197
|
if not self._session_id:
|
|
249
198
|
return
|
|
250
|
-
|
|
251
|
-
payload = {
|
|
252
|
-
"cmd": "sessions.destroy",
|
|
253
|
-
"session": self._session_id,
|
|
254
|
-
}
|
|
255
199
|
try:
|
|
256
|
-
self.
|
|
200
|
+
self._rpc.session.destroy(self._session_id)
|
|
257
201
|
except Exception:
|
|
258
202
|
return # Best-effort cleanup
|
|
259
|
-
|
|
260
203
|
self._session_created = False
|
|
261
204
|
|
|
262
205
|
|
|
@@ -273,8 +216,9 @@ class FlareSolverr(object):
|
|
|
273
216
|
version (str): FlareSolverr server version.
|
|
274
217
|
"""
|
|
275
218
|
|
|
276
|
-
def __init__(
|
|
277
|
-
|
|
219
|
+
def __init__(
|
|
220
|
+
self, status="", message="", user_agent="", start=0, end=0, version=""
|
|
221
|
+
):
|
|
278
222
|
self.status = status
|
|
279
223
|
self.message = message
|
|
280
224
|
self.user_agent = user_agent
|
|
@@ -284,7 +228,10 @@ class FlareSolverr(object):
|
|
|
284
228
|
|
|
285
229
|
def __repr__(self):
|
|
286
230
|
return "FlareSolverr(status=%r, message=%r, version=%r)" % (
|
|
287
|
-
self.status,
|
|
231
|
+
self.status,
|
|
232
|
+
self.message,
|
|
233
|
+
self.version,
|
|
234
|
+
)
|
|
288
235
|
|
|
289
236
|
|
|
290
237
|
class Response(requests.Response):
|
|
@@ -11,15 +11,16 @@ with io.open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
|
|
|
11
11
|
long_description = f.read()
|
|
12
12
|
|
|
13
13
|
package = dict()
|
|
14
|
-
with io.open(
|
|
15
|
-
|
|
14
|
+
with io.open(
|
|
15
|
+
os.path.join(this_directory, "flaresolverr_session", "__init__.py"),
|
|
16
|
+
encoding="utf-8",
|
|
17
|
+
) as f:
|
|
16
18
|
for line in f:
|
|
17
19
|
match = re.match(r"(__\w+?__)\s*=\s*(.+)$", line)
|
|
18
20
|
if match and match.group(1) != "__all__":
|
|
19
21
|
package[match.group(1)] = eval(match.group(2))
|
|
20
22
|
|
|
21
|
-
package_name = os.environ.get("FLARESOLVERR_PACKAGE_NAME",
|
|
22
|
-
package["__title__"])
|
|
23
|
+
package_name = os.environ.get("FLARESOLVERR_PACKAGE_NAME", package["__title__"])
|
|
23
24
|
|
|
24
25
|
setup(
|
|
25
26
|
name=package_name,
|
|
@@ -31,14 +32,14 @@ setup(
|
|
|
31
32
|
long_description=long_description,
|
|
32
33
|
long_description_content_type="text/markdown",
|
|
33
34
|
license="MIT",
|
|
34
|
-
|
|
35
|
+
packages=["flaresolverr_session"],
|
|
35
36
|
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
|
|
36
37
|
install_requires=[
|
|
37
38
|
"requests",
|
|
38
39
|
],
|
|
39
40
|
entry_points={
|
|
40
41
|
"console_scripts": [
|
|
41
|
-
"flaresolverr-cli=cli:main",
|
|
42
|
+
"flaresolverr-cli=flaresolverr_session.cli:main",
|
|
42
43
|
],
|
|
43
44
|
},
|
|
44
45
|
extras_require={
|
|
@@ -21,11 +21,9 @@ if sys.version_info[0] >= 3:
|
|
|
21
21
|
else:
|
|
22
22
|
from StringIO import StringIO # Python 2
|
|
23
23
|
|
|
24
|
-
from cli import main, _truncate_response_body
|
|
24
|
+
from flaresolverr_session.cli import main, _truncate_response_body
|
|
25
25
|
from flaresolverr_session import (
|
|
26
26
|
FlareSolverrResponseError,
|
|
27
|
-
FlareSolverrCaptchaError,
|
|
28
|
-
FlareSolverrTimeoutError,
|
|
29
27
|
FlareSolverrError,
|
|
30
28
|
)
|
|
31
29
|
|
|
@@ -60,6 +58,14 @@ def _fake_rpc():
|
|
|
60
58
|
"startTimestamp": 100,
|
|
61
59
|
"endTimestamp": 200,
|
|
62
60
|
}
|
|
61
|
+
rpc.session.clear.return_value = {
|
|
62
|
+
"status": "ok",
|
|
63
|
+
"message": "All sessions cleared successfully.",
|
|
64
|
+
"sessions": ["s1", "s2"],
|
|
65
|
+
"version": "3.3.21",
|
|
66
|
+
"startTimestamp": 100,
|
|
67
|
+
"endTimestamp": 200,
|
|
68
|
+
}
|
|
63
69
|
rpc.request.get.return_value = {
|
|
64
70
|
"status": "ok",
|
|
65
71
|
"message": "Challenge solved!",
|
|
@@ -107,7 +113,7 @@ def _run_cli(argv, rpc=None):
|
|
|
107
113
|
if rpc is None:
|
|
108
114
|
rpc = _fake_rpc()
|
|
109
115
|
|
|
110
|
-
with mock.patch("cli.RPC", return_value=rpc):
|
|
116
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc):
|
|
111
117
|
old_stdout = sys.stdout
|
|
112
118
|
old_stderr = sys.stderr
|
|
113
119
|
sys.stdout = captured_out = StringIO()
|
|
@@ -126,23 +132,28 @@ class TestSessionCreate(unittest.TestCase):
|
|
|
126
132
|
|
|
127
133
|
def test_create_no_args(self):
|
|
128
134
|
"""session create with no extra args."""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
rpc.session.create.assert_called_once_with(session_id=None, proxy=None)
|
|
132
|
-
data = json.loads(out)
|
|
133
|
-
self.assertEqual(data["status"], "ok")
|
|
135
|
+
with self.assertRaises(SystemExit):
|
|
136
|
+
main(["session", "create"])
|
|
134
137
|
|
|
135
138
|
def test_create_with_name(self):
|
|
136
139
|
"""session create with a session name."""
|
|
137
140
|
code, out, _err, rpc = _run_cli(["session", "create", "my-sess"])
|
|
138
141
|
self.assertEqual(code, 0)
|
|
142
|
+
self.assertEqual(rpc.session.create.call_count, 1)
|
|
139
143
|
rpc.session.create.assert_called_once_with(session_id="my-sess", proxy=None)
|
|
144
|
+
data = json.loads(out)
|
|
145
|
+
self.assertIsInstance(data, list)
|
|
146
|
+
self.assertEqual(len(data), 1)
|
|
140
147
|
|
|
141
148
|
def test_create_with_proxy(self):
|
|
142
149
|
"""session create with --proxy."""
|
|
143
|
-
code, out, _err, rpc = _run_cli(
|
|
150
|
+
code, out, _err, rpc = _run_cli(
|
|
151
|
+
["session", "create", "my-sess", "--proxy", "http://p:80"]
|
|
152
|
+
)
|
|
144
153
|
self.assertEqual(code, 0)
|
|
145
|
-
rpc.session.create.assert_called_once_with(
|
|
154
|
+
rpc.session.create.assert_called_once_with(
|
|
155
|
+
session_id="my-sess", proxy="http://p:80"
|
|
156
|
+
)
|
|
146
157
|
|
|
147
158
|
def test_create_with_name_and_proxy(self):
|
|
148
159
|
"""session create with name and proxy."""
|
|
@@ -157,15 +168,26 @@ class TestSessionCreate(unittest.TestCase):
|
|
|
157
168
|
def test_create_with_flaresolverr_url(self):
|
|
158
169
|
"""session create with -f flag."""
|
|
159
170
|
rpc = _fake_rpc()
|
|
160
|
-
with mock.patch("cli.RPC", return_value=rpc) as rpc_cls:
|
|
171
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc) as rpc_cls:
|
|
161
172
|
old_stdout = sys.stdout
|
|
162
173
|
sys.stdout = StringIO()
|
|
163
174
|
try:
|
|
164
|
-
main(["-f", "http://custom:9999/v1", "session", "create"])
|
|
175
|
+
main(["-f", "http://custom:9999/v1", "session", "create", "n1"])
|
|
165
176
|
finally:
|
|
166
177
|
sys.stdout = old_stdout
|
|
167
178
|
rpc_cls.assert_called_once_with("http://custom:9999/v1")
|
|
168
179
|
|
|
180
|
+
def test_create_with_multiple_names(self):
|
|
181
|
+
"""session create with multiple names."""
|
|
182
|
+
code, out, _err, rpc = _run_cli(["session", "create", "a", "b"])
|
|
183
|
+
self.assertEqual(code, 0)
|
|
184
|
+
self.assertEqual(rpc.session.create.call_count, 2)
|
|
185
|
+
rpc.session.create.assert_any_call(session_id="a", proxy=None)
|
|
186
|
+
rpc.session.create.assert_any_call(session_id="b", proxy=None)
|
|
187
|
+
data = json.loads(out)
|
|
188
|
+
self.assertIsInstance(data, list)
|
|
189
|
+
self.assertEqual(len(data), 2)
|
|
190
|
+
|
|
169
191
|
|
|
170
192
|
class TestSessionList(unittest.TestCase):
|
|
171
193
|
"""Tests for 'session list' CLI command."""
|
|
@@ -191,6 +213,22 @@ class TestSessionDestroy(unittest.TestCase):
|
|
|
191
213
|
self.assertEqual(data["status"], "ok")
|
|
192
214
|
|
|
193
215
|
|
|
216
|
+
class TestSessionClear(unittest.TestCase):
|
|
217
|
+
"""Tests for 'session clear' CLI command."""
|
|
218
|
+
|
|
219
|
+
def test_clear(self):
|
|
220
|
+
"""session clear invokes RPC.session.clear()."""
|
|
221
|
+
code, out, _err, rpc = _run_cli(["session", "clear"])
|
|
222
|
+
self.assertEqual(code, 0)
|
|
223
|
+
rpc.session.list.assert_called_once()
|
|
224
|
+
self.assertEqual(rpc.session.destroy.call_count, 2)
|
|
225
|
+
rpc.session.destroy.assert_any_call("s1")
|
|
226
|
+
rpc.session.destroy.assert_any_call("s2")
|
|
227
|
+
data = json.loads(out)
|
|
228
|
+
self.assertIsInstance(data, list)
|
|
229
|
+
self.assertEqual(len(data), 2)
|
|
230
|
+
|
|
231
|
+
|
|
194
232
|
class TestRequestDefault(unittest.TestCase):
|
|
195
233
|
"""Tests for the default request command (URL as first arg)."""
|
|
196
234
|
|
|
@@ -368,7 +406,7 @@ class TestRequestWithOptions(unittest.TestCase):
|
|
|
368
406
|
def test_flaresolverr_url_passed_to_rpc(self):
|
|
369
407
|
"""The -f flag is forwarded to RPC constructor."""
|
|
370
408
|
rpc = _fake_rpc()
|
|
371
|
-
with mock.patch("cli.RPC", return_value=rpc) as rpc_cls:
|
|
409
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc) as rpc_cls:
|
|
372
410
|
old_stdout = sys.stdout
|
|
373
411
|
sys.stdout = StringIO()
|
|
374
412
|
try:
|
|
@@ -380,7 +418,7 @@ class TestRequestWithOptions(unittest.TestCase):
|
|
|
380
418
|
def test_flaresolverr_url_with_request_command(self):
|
|
381
419
|
"""The -f flag works with explicit request command."""
|
|
382
420
|
rpc = _fake_rpc()
|
|
383
|
-
with mock.patch("cli.RPC", return_value=rpc) as rpc_cls:
|
|
421
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc) as rpc_cls:
|
|
384
422
|
old_stdout = sys.stdout
|
|
385
423
|
sys.stdout = StringIO()
|
|
386
424
|
try:
|
|
@@ -396,10 +434,15 @@ class TestRequestOutputFile(unittest.TestCase):
|
|
|
396
434
|
def test_output_file(self):
|
|
397
435
|
"""Response body is written to file."""
|
|
398
436
|
rpc = _fake_rpc()
|
|
399
|
-
with mock.patch("cli.RPC", return_value=rpc):
|
|
437
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc):
|
|
400
438
|
m = mock.mock_open()
|
|
401
439
|
with mock.patch(
|
|
402
|
-
|
|
440
|
+
(
|
|
441
|
+
"flaresolverr_session.cli.open"
|
|
442
|
+
if sys.version_info[0] >= 3
|
|
443
|
+
else "__builtin__.open"
|
|
444
|
+
),
|
|
445
|
+
m,
|
|
403
446
|
):
|
|
404
447
|
old_stdout = sys.stdout
|
|
405
448
|
sys.stdout = StringIO()
|
|
@@ -434,10 +477,15 @@ class TestRequestOutputFile(unittest.TestCase):
|
|
|
434
477
|
"endTimestamp": 200,
|
|
435
478
|
}
|
|
436
479
|
|
|
437
|
-
with mock.patch("cli.RPC", return_value=rpc):
|
|
480
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc):
|
|
438
481
|
m = mock.mock_open()
|
|
439
482
|
with mock.patch(
|
|
440
|
-
|
|
483
|
+
(
|
|
484
|
+
"flaresolverr_session.cli.open"
|
|
485
|
+
if sys.version_info[0] >= 3
|
|
486
|
+
else "__builtin__.open"
|
|
487
|
+
),
|
|
488
|
+
m,
|
|
441
489
|
):
|
|
442
490
|
old_stdout = sys.stdout
|
|
443
491
|
sys.stdout = StringIO()
|
|
@@ -537,7 +585,7 @@ class TestTwoPassParsing(unittest.TestCase):
|
|
|
537
585
|
def test_f_flag_before_command(self):
|
|
538
586
|
"""-f before session command."""
|
|
539
587
|
rpc = _fake_rpc()
|
|
540
|
-
with mock.patch("cli.RPC", return_value=rpc) as rpc_cls:
|
|
588
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc) as rpc_cls:
|
|
541
589
|
old_stdout = sys.stdout
|
|
542
590
|
sys.stdout = StringIO()
|
|
543
591
|
try:
|
|
@@ -549,7 +597,7 @@ class TestTwoPassParsing(unittest.TestCase):
|
|
|
549
597
|
def test_f_flag_before_url(self):
|
|
550
598
|
"""-f before URL (implicit request)."""
|
|
551
599
|
rpc = _fake_rpc()
|
|
552
|
-
with mock.patch("cli.RPC", return_value=rpc) as rpc_cls:
|
|
600
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc) as rpc_cls:
|
|
553
601
|
old_stdout = sys.stdout
|
|
554
602
|
sys.stdout = StringIO()
|
|
555
603
|
try:
|
|
@@ -562,7 +610,7 @@ class TestTwoPassParsing(unittest.TestCase):
|
|
|
562
610
|
def test_f_flag_after_url(self):
|
|
563
611
|
"""-f after URL (implicit request)."""
|
|
564
612
|
rpc = _fake_rpc()
|
|
565
|
-
with mock.patch("cli.RPC", return_value=rpc) as rpc_cls:
|
|
613
|
+
with mock.patch("flaresolverr_session.cli.RPC", return_value=rpc) as rpc_cls:
|
|
566
614
|
old_stdout = sys.stdout
|
|
567
615
|
sys.stdout = StringIO()
|
|
568
616
|
try:
|
|
@@ -652,9 +700,9 @@ class TestCliErrorHandling(unittest.TestCase):
|
|
|
652
700
|
self.assertEqual(str(e), "Connection refused")
|
|
653
701
|
|
|
654
702
|
def test_captcha_error_exits_nonzero(self):
|
|
655
|
-
"""
|
|
703
|
+
"""FlareSolverrResponseError with captcha message causes exit code 1."""
|
|
656
704
|
fake_resp = {"status": "error", "message": "Captcha detected"}
|
|
657
|
-
exc =
|
|
705
|
+
exc = FlareSolverrResponseError("Captcha detected", response_data=fake_resp)
|
|
658
706
|
rpc = self._make_rpc_raising(exc)
|
|
659
707
|
code, out, err, _ = _run_cli(["https://example.com"], rpc=rpc)
|
|
660
708
|
self.assertEqual(code, 1)
|
|
@@ -662,9 +710,9 @@ class TestCliErrorHandling(unittest.TestCase):
|
|
|
662
710
|
self.assertEqual(data["message"], "Captcha detected")
|
|
663
711
|
|
|
664
712
|
def test_timeout_error_exits_nonzero(self):
|
|
665
|
-
"""
|
|
713
|
+
"""FlareSolverrResponseError with timeout message causes exit code 1."""
|
|
666
714
|
fake_resp = {"status": "error", "message": "Error: Timeout reached"}
|
|
667
|
-
exc =
|
|
715
|
+
exc = FlareSolverrResponseError(
|
|
668
716
|
"Error: Timeout reached", response_data=fake_resp
|
|
669
717
|
)
|
|
670
718
|
rpc = self._make_rpc_raising(exc)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import unittest
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from flaresolverr_session import RPC
|
|
6
6
|
|
|
7
7
|
# Python 2/3 compatible string type check
|
|
8
8
|
try:
|
|
@@ -254,18 +254,5 @@ class TestRequestPost(RPCTestCase):
|
|
|
254
254
|
_assert_solution(result["solution"])
|
|
255
255
|
|
|
256
256
|
|
|
257
|
-
class TestRPCContextManager(unittest.TestCase):
|
|
258
|
-
def test_context_manager_returns_rpc(self):
|
|
259
|
-
"""The context manager yields the RPC instance itself."""
|
|
260
|
-
with RPC() as rpc:
|
|
261
|
-
assert isinstance(rpc, RPC)
|
|
262
|
-
|
|
263
|
-
def test_context_manager_executes_request(self):
|
|
264
|
-
"""Requests work inside a context manager block."""
|
|
265
|
-
with RPC() as rpc:
|
|
266
|
-
result = rpc.session.list()
|
|
267
|
-
_assert_ok(result)
|
|
268
|
-
|
|
269
|
-
|
|
270
257
|
if __name__ == "__main__":
|
|
271
258
|
unittest.main()
|
|
@@ -28,10 +28,9 @@ except ImportError:
|
|
|
28
28
|
import requests
|
|
29
29
|
|
|
30
30
|
from flaresolverr_session import (
|
|
31
|
-
|
|
31
|
+
FlareSolverrChallengeError,
|
|
32
32
|
FlareSolverrError,
|
|
33
33
|
FlareSolverrResponseError,
|
|
34
|
-
FlareSolverrTimeoutError,
|
|
35
34
|
FlareSolverrUnsupportedMethodError,
|
|
36
35
|
Session,
|
|
37
36
|
Response,
|
|
@@ -84,8 +83,7 @@ def _assert_response(response, entry):
|
|
|
84
83
|
|
|
85
84
|
|
|
86
85
|
def _list_sessions(session=None):
|
|
87
|
-
|
|
88
|
-
data = session.send(payload)
|
|
86
|
+
data = session._rpc.session.list()
|
|
89
87
|
return data.get("sessions", [])
|
|
90
88
|
|
|
91
89
|
|
|
@@ -157,7 +155,7 @@ class TestUnsolvedChallenge(unittest.TestCase):
|
|
|
157
155
|
"""Mock an unsolved challenge to verify exception handling."""
|
|
158
156
|
|
|
159
157
|
def test_challenge_not_solved(self):
|
|
160
|
-
"""
|
|
158
|
+
"""FlareSolverrChallengeError raised on failed challenge."""
|
|
161
159
|
fake_json = {
|
|
162
160
|
"status": "error",
|
|
163
161
|
"message": "Challenge not solved",
|
|
@@ -169,19 +167,19 @@ class TestUnsolvedChallenge(unittest.TestCase):
|
|
|
169
167
|
with _make_session() as session:
|
|
170
168
|
# Force session creation before mock
|
|
171
169
|
_ = session.session_id
|
|
172
|
-
with mock.patch.object(session._api_session, "post") as mocked_post:
|
|
170
|
+
with mock.patch.object(session._rpc._api_session, "post") as mocked_post:
|
|
173
171
|
mock_resp = mock.MagicMock()
|
|
174
172
|
mock_resp.json.return_value = fake_json
|
|
175
173
|
mocked_post.return_value = mock_resp
|
|
176
174
|
|
|
177
|
-
with self.assertRaises(
|
|
175
|
+
with self.assertRaises(FlareSolverrChallengeError) as ctx:
|
|
178
176
|
session.get("https://example.com")
|
|
179
177
|
self.assertEqual(ctx.exception.response_data, fake_json)
|
|
180
178
|
self.assertIsInstance(ctx.exception, FlareSolverrResponseError)
|
|
181
179
|
self.assertEqual(ctx.exception.message, "Challenge not solved")
|
|
182
180
|
|
|
183
181
|
def test_captcha_detected(self):
|
|
184
|
-
"""
|
|
182
|
+
"""FlareSolverrChallengeError raised when captcha is detected."""
|
|
185
183
|
fake_json = {
|
|
186
184
|
"status": "error",
|
|
187
185
|
"message": ("Captcha detected but no automatic solver is configured."),
|
|
@@ -193,18 +191,18 @@ class TestUnsolvedChallenge(unittest.TestCase):
|
|
|
193
191
|
with _make_session() as session:
|
|
194
192
|
# Force session creation before mock
|
|
195
193
|
_ = session.session_id
|
|
196
|
-
with mock.patch.object(session._api_session, "post") as mocked_post:
|
|
194
|
+
with mock.patch.object(session._rpc._api_session, "post") as mocked_post:
|
|
197
195
|
mock_resp = mock.MagicMock()
|
|
198
196
|
mock_resp.json.return_value = fake_json
|
|
199
197
|
mocked_post.return_value = mock_resp
|
|
200
198
|
|
|
201
|
-
with self.assertRaises(
|
|
199
|
+
with self.assertRaises(FlareSolverrChallengeError) as ctx:
|
|
202
200
|
session.get("https://example.com")
|
|
203
201
|
self.assertEqual(ctx.exception.response_data, fake_json)
|
|
204
202
|
self.assertIsInstance(ctx.exception, FlareSolverrResponseError)
|
|
205
203
|
|
|
206
204
|
def test_timeout_error(self):
|
|
207
|
-
"""
|
|
205
|
+
"""FlareSolverrChallengeError raised on timeout."""
|
|
208
206
|
fake_json = {
|
|
209
207
|
"status": "error",
|
|
210
208
|
"message": "Error: Timeout reached",
|
|
@@ -216,12 +214,12 @@ class TestUnsolvedChallenge(unittest.TestCase):
|
|
|
216
214
|
with _make_session() as session:
|
|
217
215
|
# Force session creation before mock
|
|
218
216
|
_ = session.session_id
|
|
219
|
-
with mock.patch.object(session._api_session, "post") as mocked_post:
|
|
217
|
+
with mock.patch.object(session._rpc._api_session, "post") as mocked_post:
|
|
220
218
|
mock_resp = mock.MagicMock()
|
|
221
219
|
mock_resp.json.return_value = fake_json
|
|
222
220
|
mocked_post.return_value = mock_resp
|
|
223
221
|
|
|
224
|
-
with self.assertRaises(
|
|
222
|
+
with self.assertRaises(FlareSolverrChallengeError) as ctx:
|
|
225
223
|
session.get("https://example.com")
|
|
226
224
|
self.assertEqual(ctx.exception.response_data, fake_json)
|
|
227
225
|
self.assertIsInstance(ctx.exception, FlareSolverrResponseError)
|
|
@@ -535,11 +533,8 @@ class TestExceptionHierarchy(unittest.TestCase):
|
|
|
535
533
|
def test_response_error_inherits_from_base(self):
|
|
536
534
|
assert issubclass(FlareSolverrResponseError, FlareSolverrError)
|
|
537
535
|
|
|
538
|
-
def
|
|
539
|
-
assert issubclass(
|
|
540
|
-
|
|
541
|
-
def test_timeout_error_inherits_from_response_error(self):
|
|
542
|
-
assert issubclass(FlareSolverrTimeoutError, FlareSolverrResponseError)
|
|
536
|
+
def test_challenge_error_inherits_from_response_error(self):
|
|
537
|
+
assert issubclass(FlareSolverrChallengeError, FlareSolverrResponseError)
|
|
543
538
|
|
|
544
539
|
def test_unsupported_method_error(self):
|
|
545
540
|
assert issubclass(FlareSolverrUnsupportedMethodError, FlareSolverrError)
|
|
@@ -550,16 +545,10 @@ class TestExceptionHierarchy(unittest.TestCase):
|
|
|
550
545
|
exc = FlareSolverrResponseError("oops", response_data=data)
|
|
551
546
|
assert exc.response_data is data
|
|
552
547
|
|
|
553
|
-
def
|
|
554
|
-
"""
|
|
555
|
-
data = {"status": "error", "message": "
|
|
556
|
-
exc =
|
|
557
|
-
assert exc.response_data is data
|
|
558
|
-
|
|
559
|
-
def test_timeout_error_carries_response_dict(self):
|
|
560
|
-
"""FlareSolverrTimeoutError stores the raw response."""
|
|
561
|
-
data = {"status": "error", "message": "timeout"}
|
|
562
|
-
exc = FlareSolverrTimeoutError("timeout", response_data=data)
|
|
548
|
+
def test_challenge_error_carries_response_dict(self):
|
|
549
|
+
"""FlareSolverrChallengeError stores the raw response."""
|
|
550
|
+
data = {"status": "error", "message": "challenge"}
|
|
551
|
+
exc = FlareSolverrChallengeError("challenge", response_data=data)
|
|
563
552
|
assert exc.response_data is data
|
|
564
553
|
|
|
565
554
|
|
|
File without changes
|
{flaresolverr_cli-0.2.1 → flaresolverr_cli-0.2.2}/flaresolverr_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|