curlifier 0.1.0__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.
- curlifier-0.1.0/LICENSE +21 -0
- curlifier-0.1.0/PKG-INFO +83 -0
- curlifier-0.1.0/README.md +66 -0
- curlifier-0.1.0/curlifier/__init__.py +5 -0
- curlifier-0.1.0/curlifier/__version__.py +31 -0
- curlifier-0.1.0/curlifier/api.py +60 -0
- curlifier-0.1.0/curlifier/configurator.py +136 -0
- curlifier-0.1.0/curlifier/curl.py +72 -0
- curlifier-0.1.0/curlifier/structures/__init__.py +0 -0
- curlifier-0.1.0/curlifier/structures/commands.py +71 -0
- curlifier-0.1.0/curlifier/structures/http_methods.py +36 -0
- curlifier-0.1.0/curlifier/structures/types.py +26 -0
- curlifier-0.1.0/curlifier/transmitter.py +184 -0
- curlifier-0.1.0/pyproject.toml +48 -0
curlifier-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Timur Valiev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
curlifier-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: curlifier
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Converts Request objects to curl string
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: curl,curlify,python requests to curl,curlifier
|
|
7
|
+
Author: Timur Valiev
|
|
8
|
+
Author-email: cptchunk@yandex.ru
|
|
9
|
+
Requires-Python: >=3.12,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: requests (>=2.30,<3.0)
|
|
15
|
+
Project-URL: Homepage, https://github.com/imtoopunkforyou/curlifier
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# curlifier
|
|
19
|
+
```
|
|
20
|
+
░█████╗░██╗░░░██╗██████╗░██╗░░░░░██╗███████╗██╗███████╗██████╗░
|
|
21
|
+
██╔══██╗██║░░░██║██╔══██╗██║░░░░░██║██╔════╝██║██╔════╝██╔══██╗
|
|
22
|
+
██║░░╚═╝██║░░░██║██████╔╝██║░░░░░██║█████╗░░██║█████╗░░██████╔╝
|
|
23
|
+
██║░░██╗██║░░░██║██╔══██╗██║░░░░░██║██╔══╝░░██║██╔══╝░░██╔══██╗
|
|
24
|
+
╚█████╔╝╚██████╔╝██║░░██║███████╗██║██║░░░░░██║███████╗██║░░██║
|
|
25
|
+
░╚════╝░░╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝╚═╝░░░░░╚═╝╚══════╝╚═╝░░╚═╝
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Curlifier converts the [Request](https://requests.readthedocs.io/en/latest/api/#requests.Response) and [PreparedRequest](https://requests.readthedocs.io/en/latest/api/#requests.PreparedRequest) objects of the [Requests](https://pypi.org/project/requests/) library into an executable [curl](https://curl.se/) command.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
Curlifier is available on PyPI:
|
|
32
|
+
```bash
|
|
33
|
+
pip install curlifier
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Dependencies
|
|
37
|
+
- `python (>=3.12, <4.0)`
|
|
38
|
+
- `requests (>=2.30.3, <3.0.0)`
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
All you need is to import `curlify`.
|
|
42
|
+
For example:
|
|
43
|
+
```python
|
|
44
|
+
>>> import requests
|
|
45
|
+
>>> from curlifier import curlify
|
|
46
|
+
>>> body = {'id': 1, 'name': 'Tima', 'age': 28}
|
|
47
|
+
>>> r = requests.post('https://httpbin.org/', json=body)
|
|
48
|
+
>>> curlify(r)
|
|
49
|
+
curl --request POST 'https://httpbin.org/' --header 'User-Agent: python-requests/2.32.3' --header 'Accept-Encoding: gzip, deflate' --header 'Accept: */*' --header 'Connection: keep-alive' --header 'Content-Type: application/json' --data '{"id": 1, "name": "Tima", "age": 28}'
|
|
50
|
+
```
|
|
51
|
+
If you use `PraparedRequest`, you can also specify it instead of the `Response` object:
|
|
52
|
+
```python
|
|
53
|
+
>>> req = requests.Request('POST', 'https://httpbin.org/')
|
|
54
|
+
>>> r = req.prepare()
|
|
55
|
+
>>> curlify(prepared_request=r)
|
|
56
|
+
curl --request POST 'https://httpbin.org/'
|
|
57
|
+
```
|
|
58
|
+
If you want a short version of the curl command, you can specify it:
|
|
59
|
+
```python
|
|
60
|
+
>>> body = {'id': 1, 'name': 'Tima', 'age': 28}
|
|
61
|
+
>>> r = requests.post('https://httpbin.org/', json=body)
|
|
62
|
+
>>> curlify(r, shorted=True)
|
|
63
|
+
curl -X POST 'https://httpbin.org/' -H 'User-Agent: python-requests/2.32.3' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' -H 'Connection: keep-alive' -H 'Content-Type: application/json' -d '{"id": 1, "name": "Tima", "age": 28}'
|
|
64
|
+
```
|
|
65
|
+
You can also specify the configuration when forming the curl command:
|
|
66
|
+
```python
|
|
67
|
+
>>> curlify(r, location=True, verbose=True, silent=True, insecure=True, include=True)
|
|
68
|
+
curl --request POST 'https://httpbin.org/' --header 'User-Agent: python-requests/2.32.3' --header 'Accept-Encoding: gzip, deflate' --header 'Accept: */*' --header 'Connection: keep-alive' --header 'Content-Type: application/json' --data '{"id": 1, "name": "Tima", "age": 28}' --location --verbose --silent --insecure --include
|
|
69
|
+
```
|
|
70
|
+
- **location** (bool) - Follow redirects (default: False)
|
|
71
|
+
- **verbose** (bool) - Verbose output (default: False)
|
|
72
|
+
- **silent** (bool) - Silent mode (default: False)
|
|
73
|
+
- **insecure** (bool) - Allow insecure connections (default: False)
|
|
74
|
+
- **include** (bool) - Include protocol headers (default: False)
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
Curlifier is released under the MIT License. See the bundled [LICENSE](LICENSE) file for details.
|
|
78
|
+
|
|
79
|
+
## **Want to Help?** ❤️
|
|
80
|
+
Feel free to pick any task from this list and contribute!
|
|
81
|
+
1. Check if an [issue](https://github.com/imtoopunkforyou/curlifier/issues) exists for the task you want to work on
|
|
82
|
+
2. If not, [open a new issue](https://github.com/imtoopunkforyou/curlifier/issues/new) to discuss
|
|
83
|
+
3. Fork the repo and create a pull request when ready
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# curlifier
|
|
2
|
+
```
|
|
3
|
+
░█████╗░██╗░░░██╗██████╗░██╗░░░░░██╗███████╗██╗███████╗██████╗░
|
|
4
|
+
██╔══██╗██║░░░██║██╔══██╗██║░░░░░██║██╔════╝██║██╔════╝██╔══██╗
|
|
5
|
+
██║░░╚═╝██║░░░██║██████╔╝██║░░░░░██║█████╗░░██║█████╗░░██████╔╝
|
|
6
|
+
██║░░██╗██║░░░██║██╔══██╗██║░░░░░██║██╔══╝░░██║██╔══╝░░██╔══██╗
|
|
7
|
+
╚█████╔╝╚██████╔╝██║░░██║███████╗██║██║░░░░░██║███████╗██║░░██║
|
|
8
|
+
░╚════╝░░╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝╚═╝░░░░░╚═╝╚══════╝╚═╝░░╚═╝
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Curlifier converts the [Request](https://requests.readthedocs.io/en/latest/api/#requests.Response) and [PreparedRequest](https://requests.readthedocs.io/en/latest/api/#requests.PreparedRequest) objects of the [Requests](https://pypi.org/project/requests/) library into an executable [curl](https://curl.se/) command.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
Curlifier is available on PyPI:
|
|
15
|
+
```bash
|
|
16
|
+
pip install curlifier
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Dependencies
|
|
20
|
+
- `python (>=3.12, <4.0)`
|
|
21
|
+
- `requests (>=2.30.3, <3.0.0)`
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
All you need is to import `curlify`.
|
|
25
|
+
For example:
|
|
26
|
+
```python
|
|
27
|
+
>>> import requests
|
|
28
|
+
>>> from curlifier import curlify
|
|
29
|
+
>>> body = {'id': 1, 'name': 'Tima', 'age': 28}
|
|
30
|
+
>>> r = requests.post('https://httpbin.org/', json=body)
|
|
31
|
+
>>> curlify(r)
|
|
32
|
+
curl --request POST 'https://httpbin.org/' --header 'User-Agent: python-requests/2.32.3' --header 'Accept-Encoding: gzip, deflate' --header 'Accept: */*' --header 'Connection: keep-alive' --header 'Content-Type: application/json' --data '{"id": 1, "name": "Tima", "age": 28}'
|
|
33
|
+
```
|
|
34
|
+
If you use `PraparedRequest`, you can also specify it instead of the `Response` object:
|
|
35
|
+
```python
|
|
36
|
+
>>> req = requests.Request('POST', 'https://httpbin.org/')
|
|
37
|
+
>>> r = req.prepare()
|
|
38
|
+
>>> curlify(prepared_request=r)
|
|
39
|
+
curl --request POST 'https://httpbin.org/'
|
|
40
|
+
```
|
|
41
|
+
If you want a short version of the curl command, you can specify it:
|
|
42
|
+
```python
|
|
43
|
+
>>> body = {'id': 1, 'name': 'Tima', 'age': 28}
|
|
44
|
+
>>> r = requests.post('https://httpbin.org/', json=body)
|
|
45
|
+
>>> curlify(r, shorted=True)
|
|
46
|
+
curl -X POST 'https://httpbin.org/' -H 'User-Agent: python-requests/2.32.3' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' -H 'Connection: keep-alive' -H 'Content-Type: application/json' -d '{"id": 1, "name": "Tima", "age": 28}'
|
|
47
|
+
```
|
|
48
|
+
You can also specify the configuration when forming the curl command:
|
|
49
|
+
```python
|
|
50
|
+
>>> curlify(r, location=True, verbose=True, silent=True, insecure=True, include=True)
|
|
51
|
+
curl --request POST 'https://httpbin.org/' --header 'User-Agent: python-requests/2.32.3' --header 'Accept-Encoding: gzip, deflate' --header 'Accept: */*' --header 'Connection: keep-alive' --header 'Content-Type: application/json' --data '{"id": 1, "name": "Tima", "age": 28}' --location --verbose --silent --insecure --include
|
|
52
|
+
```
|
|
53
|
+
- **location** (bool) - Follow redirects (default: False)
|
|
54
|
+
- **verbose** (bool) - Verbose output (default: False)
|
|
55
|
+
- **silent** (bool) - Silent mode (default: False)
|
|
56
|
+
- **insecure** (bool) - Allow insecure connections (default: False)
|
|
57
|
+
- **include** (bool) - Include protocol headers (default: False)
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
Curlifier is released under the MIT License. See the bundled [LICENSE](LICENSE) file for details.
|
|
61
|
+
|
|
62
|
+
## **Want to Help?** ❤️
|
|
63
|
+
Feel free to pick any task from this list and contribute!
|
|
64
|
+
1. Check if an [issue](https://github.com/imtoopunkforyou/curlifier/issues) exists for the task you want to work on
|
|
65
|
+
2. If not, [open a new issue](https://github.com/imtoopunkforyou/curlifier/issues/new) to discuss
|
|
66
|
+
3. Fork the repo and create a pull request when ready
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ░█████╗░██╗░░░██╗██████╗░██╗░░░░░██╗███████╗██╗███████╗██████╗░
|
|
2
|
+
# ██╔══██╗██║░░░██║██╔══██╗██║░░░░░██║██╔════╝██║██╔════╝██╔══██╗
|
|
3
|
+
# ██║░░╚═╝██║░░░██║██████╔╝██║░░░░░██║█████╗░░██║█████╗░░██████╔╝
|
|
4
|
+
# ██║░░██╗██║░░░██║██╔══██╗██║░░░░░██║██╔══╝░░██║██╔══╝░░██╔══██╗
|
|
5
|
+
# ╚█████╔╝╚██████╔╝██║░░██║███████╗██║██║░░░░░██║███████╗██║░░██║
|
|
6
|
+
# ░╚════╝░░╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝╚═╝░░░░░╚═╝╚══════╝╚═╝░░╚═╝
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageMetadata, metadata
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
pkg_name: Final[str] = str(Path(__file__).parent.name)
|
|
13
|
+
pkg_data: PackageMetadata = metadata(pkg_name)
|
|
14
|
+
|
|
15
|
+
NAME: Final[str] = pkg_data['Name']
|
|
16
|
+
VERSION: Final[str] = pkg_data['Version']
|
|
17
|
+
AUTHOR: Final[str] = pkg_data['Author']
|
|
18
|
+
AUTHOR_EMAIL: Final[str] = pkg_data['Author-email']
|
|
19
|
+
LICENSE: Final[str] = pkg_data['License']
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_package_information() -> dict[str, str]:
|
|
23
|
+
pkg_info = {
|
|
24
|
+
'name': NAME,
|
|
25
|
+
'version': VERSION,
|
|
26
|
+
'author': AUTHOR,
|
|
27
|
+
'author_email': AUTHOR_EMAIL,
|
|
28
|
+
'license': LICENSE,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return pkg_info
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Unpack
|
|
2
|
+
|
|
3
|
+
from requests.models import PreparedRequest, Response
|
|
4
|
+
|
|
5
|
+
from curlifier.curl import CurlBuilder
|
|
6
|
+
from curlifier.structures.types import CurlifyConfigure
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def curlify(
|
|
10
|
+
response: Response | None = None,
|
|
11
|
+
*,
|
|
12
|
+
prepared_request: PreparedRequest | None = None,
|
|
13
|
+
shorted: bool = False,
|
|
14
|
+
**config: Unpack[CurlifyConfigure],
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
The only correct entry point of the `curlifier` library.
|
|
18
|
+
|
|
19
|
+
:param response: The `requests` library Response object.
|
|
20
|
+
Must be specified if the `prepared_request` argument is not specified.
|
|
21
|
+
:type response: Response | None, optional
|
|
22
|
+
|
|
23
|
+
:param prepared_request: The `requests` library `PreparedRequest` object.
|
|
24
|
+
Must be specified if the `response` argument is not specified.
|
|
25
|
+
:type prepared_request: PreparedRequest | None, optional
|
|
26
|
+
|
|
27
|
+
:param shorted: Specify `True` if you want to build the curl command in a shortened form.
|
|
28
|
+
Otherwise `False`. Defaults to `False`.
|
|
29
|
+
:type shorted: bool
|
|
30
|
+
|
|
31
|
+
:param config: Additional configuration options for curl command:
|
|
32
|
+
- **location** (bool) - Follow redirects (default: False)
|
|
33
|
+
- **verbose** (bool) - Verbose output (default: False)
|
|
34
|
+
- **silent** (bool) - Silent mode (default: False)
|
|
35
|
+
- **insecure** (bool) - Allow insecure connections (default: False)
|
|
36
|
+
- **include** (bool) - Include protocol headers (default: False)
|
|
37
|
+
:type config: Unpack[CurlifyConfigure]
|
|
38
|
+
|
|
39
|
+
:return: Executable curl command.
|
|
40
|
+
:rtype: str
|
|
41
|
+
|
|
42
|
+
>>> import requests
|
|
43
|
+
>>> from curlifier import curlify
|
|
44
|
+
>>> r = requests.get('https://example.com/')
|
|
45
|
+
>>> curlify(r, shorted=True)
|
|
46
|
+
"curl -X GET 'https://example.com/' -H 'User-Agent: python-requests/2.32.3' <...>"
|
|
47
|
+
"""
|
|
48
|
+
curl_builder = CurlBuilder(
|
|
49
|
+
response=response,
|
|
50
|
+
prepared_request=prepared_request,
|
|
51
|
+
build_short=shorted,
|
|
52
|
+
location=config.pop('location', False),
|
|
53
|
+
verbose=config.pop('verbose', False),
|
|
54
|
+
silent=config.pop('silent', False),
|
|
55
|
+
insecure=config.pop('insecure', False),
|
|
56
|
+
include=config.pop('include', False),
|
|
57
|
+
)
|
|
58
|
+
curl: str = curl_builder.build()
|
|
59
|
+
|
|
60
|
+
return curl
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from typing import Generator, Self
|
|
2
|
+
|
|
3
|
+
from curlifier.structures.commands import CommandsConfigureEnum
|
|
4
|
+
from curlifier.structures.types import CurlCommand, EmptyStr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Config:
|
|
8
|
+
"""Parameters for curl command configuration."""
|
|
9
|
+
|
|
10
|
+
__slots__ = (
|
|
11
|
+
'_location',
|
|
12
|
+
'_verbose',
|
|
13
|
+
'_silent',
|
|
14
|
+
'_insecure',
|
|
15
|
+
'_include',
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self: Self,
|
|
20
|
+
location: bool,
|
|
21
|
+
verbose: bool,
|
|
22
|
+
silent: bool,
|
|
23
|
+
insecure: bool,
|
|
24
|
+
include: bool,
|
|
25
|
+
) -> None:
|
|
26
|
+
self._location = location
|
|
27
|
+
self._verbose = verbose
|
|
28
|
+
self._silent = silent
|
|
29
|
+
self._insecure = insecure
|
|
30
|
+
self._include = include
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def location(self: Self) -> bool:
|
|
34
|
+
"""Follow redirects."""
|
|
35
|
+
return self._location
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def verbose(self: Self) -> bool:
|
|
39
|
+
"""Make the operation more talkative."""
|
|
40
|
+
return self._verbose
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def silent(self: Self) -> bool:
|
|
44
|
+
"""Silent mode."""
|
|
45
|
+
return self._silent
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def insecure(self: Self) -> bool:
|
|
49
|
+
"""Allow insecure server connections."""
|
|
50
|
+
return self._insecure
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def include(self: Self) -> bool:
|
|
54
|
+
"""Include protocol response headers in the output."""
|
|
55
|
+
return self._include
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ConfigBuilder(Config):
|
|
59
|
+
"""Builds a curl command configuration line."""
|
|
60
|
+
|
|
61
|
+
__slots__ = (
|
|
62
|
+
'build_short',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self: Self,
|
|
67
|
+
build_short: bool = False,
|
|
68
|
+
**kwargs: bool,
|
|
69
|
+
) -> None:
|
|
70
|
+
self.build_short = build_short
|
|
71
|
+
super().__init__(**kwargs)
|
|
72
|
+
|
|
73
|
+
def build(self: Self) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Collects all parameters into the resulting string.
|
|
76
|
+
|
|
77
|
+
If `build_short` is `True` will be collected short version.
|
|
78
|
+
|
|
79
|
+
>>> from curlifier.configurator import ConfigBuilder
|
|
80
|
+
>>> conf = ConfigBuilder(
|
|
81
|
+
location=True,
|
|
82
|
+
verbose=True,
|
|
83
|
+
silent=False,
|
|
84
|
+
insecure=True,
|
|
85
|
+
include=False,
|
|
86
|
+
build_short=False,
|
|
87
|
+
)
|
|
88
|
+
>>> conf.build()
|
|
89
|
+
'--location --verbose --insecure'
|
|
90
|
+
"""
|
|
91
|
+
commands: tuple[CurlCommand | EmptyStr, ...] = (
|
|
92
|
+
self.get_location_command(),
|
|
93
|
+
self.get_verbose_command(),
|
|
94
|
+
self.get_silent_command(),
|
|
95
|
+
self.get_insecure_command(),
|
|
96
|
+
self.get_include_command(),
|
|
97
|
+
)
|
|
98
|
+
cleaned_commands: Generator[CurlCommand, None, None] = (
|
|
99
|
+
command for command in commands if command
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return ' '.join(cleaned_commands)
|
|
103
|
+
|
|
104
|
+
def get_location_command(self: Self) -> CurlCommand | EmptyStr:
|
|
105
|
+
if self.location:
|
|
106
|
+
command = CommandsConfigureEnum.LOCATION.get(shorted=self.build_short)
|
|
107
|
+
return command
|
|
108
|
+
|
|
109
|
+
return ''
|
|
110
|
+
|
|
111
|
+
def get_verbose_command(self: Self) -> CurlCommand | EmptyStr:
|
|
112
|
+
if self.verbose:
|
|
113
|
+
command = CommandsConfigureEnum.VERBOSE.get(shorted=self.build_short)
|
|
114
|
+
return command
|
|
115
|
+
return ''
|
|
116
|
+
|
|
117
|
+
def get_silent_command(self: Self) -> CurlCommand | EmptyStr:
|
|
118
|
+
if self.silent:
|
|
119
|
+
command = CommandsConfigureEnum.SILENT.get(shorted=self.build_short)
|
|
120
|
+
return command
|
|
121
|
+
|
|
122
|
+
return ''
|
|
123
|
+
|
|
124
|
+
def get_insecure_command(self: Self) -> CurlCommand | EmptyStr:
|
|
125
|
+
if self.insecure:
|
|
126
|
+
command = CommandsConfigureEnum.INSECURE.get(shorted=self.build_short)
|
|
127
|
+
return command
|
|
128
|
+
|
|
129
|
+
return ''
|
|
130
|
+
|
|
131
|
+
def get_include_command(self: Self) -> CurlCommand | EmptyStr:
|
|
132
|
+
if self.include:
|
|
133
|
+
command = CommandsConfigureEnum.INCLUDE.get(shorted=self.build_short)
|
|
134
|
+
return command
|
|
135
|
+
|
|
136
|
+
return ''
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from requests.models import PreparedRequest, Response
|
|
4
|
+
|
|
5
|
+
from curlifier.configurator import ConfigBuilder
|
|
6
|
+
from curlifier.transmitter import TransmitterBuilder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CurlBuilder:
|
|
10
|
+
"""Builds the executable curl command."""
|
|
11
|
+
|
|
12
|
+
curl_command = 'curl'
|
|
13
|
+
|
|
14
|
+
def __init__( # noqa: WPS211
|
|
15
|
+
self: Self,
|
|
16
|
+
location: bool,
|
|
17
|
+
verbose: bool,
|
|
18
|
+
silent: bool,
|
|
19
|
+
insecure: bool,
|
|
20
|
+
include: bool,
|
|
21
|
+
build_short: bool,
|
|
22
|
+
response: Response | None = None,
|
|
23
|
+
prepared_request: PreparedRequest | None = None,
|
|
24
|
+
|
|
25
|
+
) -> None:
|
|
26
|
+
self.build_short = build_short
|
|
27
|
+
self.config = ConfigBuilder(
|
|
28
|
+
build_short=self.build_short,
|
|
29
|
+
location=location,
|
|
30
|
+
verbose=verbose,
|
|
31
|
+
silent=silent,
|
|
32
|
+
insecure=insecure,
|
|
33
|
+
include=include,
|
|
34
|
+
)
|
|
35
|
+
self.transmitter = TransmitterBuilder(
|
|
36
|
+
response=response,
|
|
37
|
+
prepared_request=prepared_request,
|
|
38
|
+
build_short=self.build_short,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def build(self: Self) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Collects all parameters into the resulting string.
|
|
44
|
+
|
|
45
|
+
If `build_short` is `True` will be collected short version.
|
|
46
|
+
|
|
47
|
+
>>> from curlifier.curl import CurlBuilder
|
|
48
|
+
>>> import requests
|
|
49
|
+
>>> r = requests.get('https://example.com/')
|
|
50
|
+
>>> curl_builder = CurlBuilder(
|
|
51
|
+
response=r,
|
|
52
|
+
location=True,
|
|
53
|
+
build_short=True,
|
|
54
|
+
verbose=False,
|
|
55
|
+
silent=False,
|
|
56
|
+
insecure=False,
|
|
57
|
+
include=False,
|
|
58
|
+
)
|
|
59
|
+
>>> curl_builder.build()
|
|
60
|
+
"curl -X GET 'https://example.com/' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' <...> -L"
|
|
61
|
+
"""
|
|
62
|
+
builded_config: str = self.config.build()
|
|
63
|
+
builded_transmitter: str = self.transmitter.build()
|
|
64
|
+
builded: str = ' '.join(
|
|
65
|
+
(
|
|
66
|
+
self.curl_command,
|
|
67
|
+
builded_transmitter,
|
|
68
|
+
builded_config,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return builded
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from curlifier.structures.types import (
|
|
5
|
+
CurlCommand,
|
|
6
|
+
CurlCommandLong,
|
|
7
|
+
CurlCommandShort,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandsEnum(enum.Enum):
|
|
12
|
+
"""
|
|
13
|
+
Base class of the command curl structure.
|
|
14
|
+
When initialized, it will take two values: short and long.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self: Self, short: CurlCommandShort, long: CurlCommandLong) -> None:
|
|
18
|
+
self.short = short
|
|
19
|
+
self.long = long
|
|
20
|
+
|
|
21
|
+
def get(self: Self, *, shorted: bool) -> CurlCommand:
|
|
22
|
+
"""
|
|
23
|
+
Returns curl command.
|
|
24
|
+
|
|
25
|
+
:param shorted: `True` if you need a short version of the command. Otherwise `False`.
|
|
26
|
+
:type shorted: bool
|
|
27
|
+
|
|
28
|
+
:return: Curl command.
|
|
29
|
+
:rtype: CurlCommand
|
|
30
|
+
"""
|
|
31
|
+
return self.short if shorted else self.long
|
|
32
|
+
|
|
33
|
+
def __str__(self: Self) -> CurlCommandLong:
|
|
34
|
+
return self.long
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@enum.unique
|
|
38
|
+
class CommandsConfigureEnum(CommandsEnum):
|
|
39
|
+
"""Curl configuration commands."""
|
|
40
|
+
|
|
41
|
+
VERBOSE = ('-v', '--verbose')
|
|
42
|
+
"""Make the operation more talkative."""
|
|
43
|
+
|
|
44
|
+
SILENT = ('-s', '--silent')
|
|
45
|
+
"""Silent mode."""
|
|
46
|
+
|
|
47
|
+
INSECURE = ('-k', '--insecure')
|
|
48
|
+
"""Allow insecure server connections."""
|
|
49
|
+
|
|
50
|
+
LOCATION = ('-L', '--location')
|
|
51
|
+
"""Follow redirects."""
|
|
52
|
+
|
|
53
|
+
INCLUDE = ('-i', '--include')
|
|
54
|
+
"""Include protocol response headers in the output."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@enum.unique
|
|
58
|
+
class CommandsTransferEnum(CommandsEnum):
|
|
59
|
+
"""Curl transfer commands."""
|
|
60
|
+
|
|
61
|
+
SEND_DATA = ('-d', '--data')
|
|
62
|
+
"""HTTP data (body)."""
|
|
63
|
+
|
|
64
|
+
HEADER = ('-H', '--header')
|
|
65
|
+
"""Pass custom header(s) to server."""
|
|
66
|
+
|
|
67
|
+
REQUEST = ('-X', '--request')
|
|
68
|
+
"""Specify request method to use."""
|
|
69
|
+
|
|
70
|
+
FORM = ('-F', '--form')
|
|
71
|
+
"""Specify multipart MIME data."""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from curlifier.structures.types import HttpMethod
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@enum.unique
|
|
8
|
+
class HttpMethodsEnum(enum.Enum):
|
|
9
|
+
"""Supported HTTP methods."""
|
|
10
|
+
|
|
11
|
+
GET = 'GET'
|
|
12
|
+
OPTIONS = 'OPTIONS'
|
|
13
|
+
HEAD = 'HEAD'
|
|
14
|
+
POST = 'POST'
|
|
15
|
+
PUT = 'PUT'
|
|
16
|
+
PATCH = 'PATCH'
|
|
17
|
+
DELETE = 'DELETE'
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_methods_without_body(cls: type[Self]) -> tuple[HttpMethod, HttpMethod, HttpMethod, HttpMethod]:
|
|
21
|
+
"""HTTP methods methods that have a body in the structure."""
|
|
22
|
+
return (
|
|
23
|
+
cls.GET.value,
|
|
24
|
+
cls.HEAD.value,
|
|
25
|
+
cls.DELETE.value,
|
|
26
|
+
cls.OPTIONS.value,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_methods_with_body(cls: type[Self]) -> tuple[HttpMethod, HttpMethod, HttpMethod]:
|
|
31
|
+
"""HTTP methods that do not have a body in the structure"""
|
|
32
|
+
return (
|
|
33
|
+
cls.POST.value,
|
|
34
|
+
cls.PUT.value,
|
|
35
|
+
cls.PATCH.value,
|
|
36
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Any, Literal, TypedDict
|
|
2
|
+
|
|
3
|
+
from requests.structures import CaseInsensitiveDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CurlifyConfigure(TypedDict, total=False):
|
|
7
|
+
location: bool
|
|
8
|
+
verbose: bool
|
|
9
|
+
silent: bool
|
|
10
|
+
insecure: bool
|
|
11
|
+
include: bool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type HeaderKey = str
|
|
15
|
+
type HeaderValue = str
|
|
16
|
+
type CurlCommandShort = str
|
|
17
|
+
type CurlCommandLong = str
|
|
18
|
+
type CurlCommand = CurlCommandShort | CurlCommandLong
|
|
19
|
+
type HttpMethod = str
|
|
20
|
+
type PreReqHttpMethod = str | Any | None
|
|
21
|
+
type PreReqHttpBody = bytes | str | Any | None
|
|
22
|
+
type PreReqHttpHeaders = CaseInsensitiveDict
|
|
23
|
+
type PreReqHttpUrl = str | Any | None
|
|
24
|
+
type FileNameWithExtension = str
|
|
25
|
+
type FileFieldName = str
|
|
26
|
+
type EmptyStr = Literal['']
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import re
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from requests import PreparedRequest, Response
|
|
6
|
+
|
|
7
|
+
from curlifier.structures.commands import CommandsTransferEnum
|
|
8
|
+
from curlifier.structures.http_methods import HttpMethodsEnum
|
|
9
|
+
from curlifier.structures.types import (
|
|
10
|
+
EmptyStr,
|
|
11
|
+
FileFieldName,
|
|
12
|
+
FileNameWithExtension,
|
|
13
|
+
HeaderKey,
|
|
14
|
+
PreReqHttpBody,
|
|
15
|
+
PreReqHttpHeaders,
|
|
16
|
+
PreReqHttpMethod,
|
|
17
|
+
PreReqHttpUrl,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PreparedTransmitter:
|
|
22
|
+
"""
|
|
23
|
+
Prepares request data for processing.
|
|
24
|
+
|
|
25
|
+
Works on a copy of the request object. The original object will not be modified.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self: Self,
|
|
30
|
+
response: Response | None = None,
|
|
31
|
+
*,
|
|
32
|
+
prepared_request: PreparedRequest | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
if sum(arg is not None for arg in (response, prepared_request)) != 1:
|
|
35
|
+
raise ValueError("Only one argument must be specified: `response` or `prepared_request`")
|
|
36
|
+
self._pre_req: PreparedRequest = (
|
|
37
|
+
prepared_request.copy() if response is None # type: ignore [union-attr]
|
|
38
|
+
else response.request.copy()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self._method: PreReqHttpMethod = self._pre_req.method
|
|
42
|
+
self._body: PreReqHttpBody = self._pre_req.body
|
|
43
|
+
self._headers: PreReqHttpHeaders = self._pre_req.headers
|
|
44
|
+
self._url: PreReqHttpUrl = self._pre_req.url
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def url(self: Self) -> PreReqHttpUrl:
|
|
48
|
+
return self._url
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def method(self: Self) -> PreReqHttpMethod:
|
|
52
|
+
return self._method
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def body(self: Self) -> PreReqHttpBody:
|
|
56
|
+
return self._body
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def headers(self: Self) -> PreReqHttpHeaders:
|
|
60
|
+
cleared_headers = copy.deepcopy(self._headers)
|
|
61
|
+
trash_headers: tuple[HeaderKey] = (
|
|
62
|
+
'Content-Length',
|
|
63
|
+
)
|
|
64
|
+
for header in trash_headers:
|
|
65
|
+
cleared_headers.pop(header, None)
|
|
66
|
+
|
|
67
|
+
if 'boundary=' in cleared_headers.get('Content-Type', ''):
|
|
68
|
+
cleared_headers['Content-Type'] = 'multipart/form-data'
|
|
69
|
+
|
|
70
|
+
return cleared_headers
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def has_body(self: Self) -> bool:
|
|
74
|
+
if self._pre_req.method in HttpMethodsEnum.get_methods_with_body():
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TransmitterBuilder(PreparedTransmitter):
|
|
81
|
+
"""Builds a curl command transfer line."""
|
|
82
|
+
|
|
83
|
+
executable_part = '{request_command} {method} \'{url}\' {request_headers} {request_data}'
|
|
84
|
+
executable_request_data = '{command} \'{request_data}\''
|
|
85
|
+
executable_header = '{command} \'{key}: {value}\''
|
|
86
|
+
executable_request_files = '{command} \'{field_name}=@{file_name}\''
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self: Self,
|
|
90
|
+
build_short: bool,
|
|
91
|
+
response: Response | None = None,
|
|
92
|
+
prepared_request: PreparedRequest | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
self.build_short = build_short
|
|
95
|
+
super().__init__(response, prepared_request=prepared_request)
|
|
96
|
+
|
|
97
|
+
def build(self: Self) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Collects all parameters into the resulting string.
|
|
100
|
+
|
|
101
|
+
If `build_short` is `True` will be collected short version.
|
|
102
|
+
|
|
103
|
+
>>> from curlifier.transmitter import TransmitterBuilder
|
|
104
|
+
>>> import requests
|
|
105
|
+
>>> r = requests.get('https://example.com/')
|
|
106
|
+
>>> t = TransmitterBuilder(response=r, build_short=False)
|
|
107
|
+
>>> t.build()
|
|
108
|
+
"--request GET 'https://example.com/' --header 'User-Agent: python-requests/2.32.3' <...>"
|
|
109
|
+
"""
|
|
110
|
+
request_command = CommandsTransferEnum.REQUEST.get(shorted=self.build_short)
|
|
111
|
+
request_headers = self._build_executable_headers()
|
|
112
|
+
request_data = self._build_request_data()
|
|
113
|
+
|
|
114
|
+
return self.executable_part.format(
|
|
115
|
+
request_command=request_command,
|
|
116
|
+
method=self.method,
|
|
117
|
+
url=self.url,
|
|
118
|
+
request_headers=request_headers,
|
|
119
|
+
request_data=request_data,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _build_executable_headers(self: Self) -> str:
|
|
123
|
+
return ' '.join(
|
|
124
|
+
self.executable_header.format(
|
|
125
|
+
command=CommandsTransferEnum.HEADER.get(shorted=self.build_short),
|
|
126
|
+
key=header_key,
|
|
127
|
+
value=header_value,
|
|
128
|
+
) for header_key, header_value in self.headers.items()
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _decode_files(self: Self) -> tuple[tuple[FileFieldName, FileNameWithExtension], ...] | None:
|
|
132
|
+
re_expression = rb'name="([^"]+).*?filename="([^"]+)'
|
|
133
|
+
matches = re.findall(
|
|
134
|
+
re_expression,
|
|
135
|
+
self.body, # type: ignore [arg-type]
|
|
136
|
+
flags=re.DOTALL
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return tuple(
|
|
140
|
+
(
|
|
141
|
+
field_name.decode(),
|
|
142
|
+
file_name.decode(),
|
|
143
|
+
) for field_name, file_name in matches
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _decode_raw(self: Self) -> str:
|
|
147
|
+
re_expression = r'\s+'
|
|
148
|
+
|
|
149
|
+
return re.sub(re_expression, ' ', str(self.body)).strip()
|
|
150
|
+
|
|
151
|
+
def _decode_body(
|
|
152
|
+
self: Self,
|
|
153
|
+
) -> None | tuple[tuple[FileFieldName, FileNameWithExtension], ...] | str:
|
|
154
|
+
if isinstance(self.body, bytes):
|
|
155
|
+
try:
|
|
156
|
+
return self.body.decode('utf-8')
|
|
157
|
+
except UnicodeDecodeError:
|
|
158
|
+
return self._decode_files()
|
|
159
|
+
elif isinstance(self.body, str):
|
|
160
|
+
return self._decode_raw()
|
|
161
|
+
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def _build_request_data(
|
|
165
|
+
self: Self,
|
|
166
|
+
) -> str | EmptyStr:
|
|
167
|
+
if self.has_body:
|
|
168
|
+
decode_body = self._decode_body()
|
|
169
|
+
if isinstance(decode_body, str):
|
|
170
|
+
return self.executable_request_data.format(
|
|
171
|
+
command=CommandsTransferEnum.SEND_DATA.get(shorted=self.build_short),
|
|
172
|
+
request_data=decode_body,
|
|
173
|
+
)
|
|
174
|
+
elif isinstance(decode_body, tuple):
|
|
175
|
+
executable_files: str = ' '.join(
|
|
176
|
+
self.executable_request_files.format(
|
|
177
|
+
command=CommandsTransferEnum.FORM.get(shorted=self.build_short),
|
|
178
|
+
field_name=field_name,
|
|
179
|
+
file_name=file_name,
|
|
180
|
+
) for field_name, file_name in decode_body
|
|
181
|
+
)
|
|
182
|
+
return executable_files
|
|
183
|
+
|
|
184
|
+
return ''
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "curlifier"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Converts Request objects to curl string"
|
|
9
|
+
authors = [
|
|
10
|
+
"Timur Valiev <cptchunk@yandex.ru>"
|
|
11
|
+
]
|
|
12
|
+
license = "MIT"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
|
|
15
|
+
homepage = "https://github.com/imtoopunkforyou/curlifier"
|
|
16
|
+
|
|
17
|
+
keywords = [
|
|
18
|
+
"curl",
|
|
19
|
+
"curlify",
|
|
20
|
+
"python requests to curl",
|
|
21
|
+
"curlifier",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.poetry.dependencies]
|
|
25
|
+
python = "^3.12"
|
|
26
|
+
requests = "^2.30"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
[tool.poetry.group.lint.dependencies]
|
|
30
|
+
wemake-python-styleguide = "^1.1.0"
|
|
31
|
+
mypy = "^1.15.0"
|
|
32
|
+
isort = "^6.0.1"
|
|
33
|
+
types-requests = "^2.32.0.20250328"
|
|
34
|
+
darglint = "^1.8.1"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.dev.dependencies]
|
|
38
|
+
nitpick = "^0.35.0"
|
|
39
|
+
ipython = "^9.1.0"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
[tool.poetry.group.tests.dependencies]
|
|
43
|
+
pytest = "^8.3.5"
|
|
44
|
+
faker = "^37.1.0"
|
|
45
|
+
pytest-cov = "^6.1.1"
|
|
46
|
+
|
|
47
|
+
[tool.nitpick]
|
|
48
|
+
style = "https://raw.githubusercontent.com/imtoopunkforyou/itpfy-style/refs/heads/main/py/imtoopunkforyou.toml"
|