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.
@@ -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.
@@ -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,5 @@
1
+ from curlifier.api import curlify
2
+
3
+ __all__ = (
4
+ 'curlify',
5
+ )
@@ -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"