curlify3 0.1rc1__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,37 @@
1
+ Metadata-Version: 2.1
2
+ Name: curlify3
3
+ Version: 0.1rc1
4
+ Summary: yet another python Request objects curlificator
5
+ Author: A.Shpak
6
+ Author-email: aleksander.shpak.als@gmail.com
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Description-Content-Type: text/markdown
15
+
16
+ # yet another library to convert python requests request object to curl command
17
+
18
+ [![PyPI](https://img.shields.io/pypi/v/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
19
+ [![PyPI](https://img.shields.io/pypi/dm/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ pip install curlify3
25
+ ```
26
+
27
+ ## Example
28
+
29
+ ```py
30
+ from curlify3 import to_curl
31
+ import requests
32
+
33
+ response = requests.get("http://google.ru")
34
+ print(to_curl(response.request))
35
+ # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' -H 'accept: */*' -H 'connection: keep-alive' http://www.google.ru/
36
+ ```
37
+
@@ -0,0 +1,21 @@
1
+ # yet another library to convert python requests request object to curl command
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
4
+ [![PyPI](https://img.shields.io/pypi/dm/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
5
+
6
+ ## Installation
7
+
8
+ ```sh
9
+ pip install curlify3
10
+ ```
11
+
12
+ ## Example
13
+
14
+ ```py
15
+ from curlify3 import to_curl
16
+ import requests
17
+
18
+ response = requests.get("http://google.ru")
19
+ print(to_curl(response.request))
20
+ # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' -H 'accept: */*' -H 'connection: keep-alive' http://www.google.ru/
21
+ ```
@@ -0,0 +1,7 @@
1
+ from curlify3._curl import to_curl, to_curl_async
2
+
3
+ __version__ = "0.1rc1"
4
+ __all__ = [
5
+ "to_curl",
6
+ "to_curl_async",
7
+ ]
@@ -0,0 +1,40 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class BaseRequestData(ABC):
5
+
6
+ def __init__(self, request) -> None:
7
+ if not isinstance(request, self._instance_of):
8
+ raise ValueError
9
+ self._request = request
10
+
11
+ @property
12
+ def url(self):
13
+ return str(self._request.url)
14
+
15
+ @property
16
+ def method(self):
17
+ return self._request.method
18
+
19
+ @property
20
+ def headers(self):
21
+ headers = {name.lower(): value for name, value in dict(self._request.headers).items()}
22
+ if self._request.headers.get("cookie"):
23
+ del headers["cookie"]
24
+ return headers
25
+
26
+ @property
27
+ def cookies(self):
28
+ if "cookie" not in self._request.headers:
29
+ return None
30
+ return self._request.headers.get("cookie")
31
+
32
+ @abstractmethod
33
+ def body(self):
34
+ raise NotImplementedError
35
+
36
+
37
+ class AsyncBaseRequestData(BaseRequestData, ABC):
38
+ @abstractmethod
39
+ async def body(self):
40
+ raise NotImplementedError
@@ -0,0 +1,90 @@
1
+ import re
2
+
3
+ from asyncio import iscoroutine
4
+
5
+ from curlify3._utils import make_request_obj, make_request_obj_async
6
+
7
+ MULTIPART_FORM_DATA = re.compile(rb'form-data; name="(.[^"]+)"\r\n\r\n(.+)\r\n')
8
+ MULTIPART_FILE_DATA = re.compile(rb'form-data; name="(.[^"]+)"; filename="(.[^"]+)"')
9
+
10
+
11
+ def make_full_url(url) -> str:
12
+ return url if not "&" in url else f"'{url}'"
13
+
14
+
15
+ def make_curl_headers(headers):
16
+ results = []
17
+ for header, value in headers.items():
18
+ results.append(f"-H '{header}: {value}'")
19
+ return " ".join(results)
20
+
21
+
22
+ def make_curl_cookies(cookies):
23
+ if not cookies:
24
+ return None
25
+ if " " in cookies:
26
+ cookies = f"'{cookies}'"
27
+ return f"-b {cookies}"
28
+
29
+
30
+ def make_multipart_curl_args(body):
31
+ body_parts = []
32
+ for matched in MULTIPART_FORM_DATA.finditer(body):
33
+ groups = matched.groups()
34
+ body_parts.append(f"-F '{groups[0].decode()}={groups[1].decode()}'")
35
+ for matched in MULTIPART_FILE_DATA.finditer(body):
36
+ groups = matched.groups()
37
+ body_parts.append(f"-F '{groups[0].decode()}=@{groups[1].decode()}'")
38
+ return " ".join(body_parts)
39
+
40
+
41
+ def make_curl_body(
42
+ body,
43
+ headers,
44
+ ):
45
+ if "multipart" in headers.get("content-type", ""):
46
+ return make_multipart_curl_args(body)
47
+ if not body:
48
+ return ""
49
+ return f"-d '{body}'"
50
+
51
+
52
+ def make_curl_string(method, url, headers, body, cookies):
53
+ if "content-length" in headers:
54
+ del headers["content-length"]
55
+ if body and isinstance(body, (str, bytes)) and not headers.get("content-type"):
56
+ headers["content-type"] = "plain/text"
57
+ cli_parts = [
58
+ f"curl",
59
+ f"-X {method}" if method != "GET" else None,
60
+ make_curl_cookies(cookies),
61
+ make_curl_headers(headers),
62
+ make_curl_body(body, headers),
63
+ make_full_url(url),
64
+ ]
65
+ return " ".join([str(entity) for entity in cli_parts if entity])
66
+
67
+
68
+ def to_curl(request):
69
+ data = make_request_obj(request)
70
+ return make_curl_string(
71
+ method=data.method,
72
+ url=data.url,
73
+ headers=data.headers,
74
+ body=data.body(),
75
+ cookies=data.cookies,
76
+ )
77
+
78
+
79
+ async def to_curl_async(request):
80
+ data = make_request_obj_async(request)
81
+ body = data.body()
82
+ if iscoroutine(body):
83
+ body = await body
84
+ return make_curl_string(
85
+ method=data.method,
86
+ url=data.url,
87
+ headers=data.headers,
88
+ body=body,
89
+ cookies=data.cookies,
90
+ )
@@ -0,0 +1,27 @@
1
+ import httpx
2
+
3
+ from curlify3._base import AsyncBaseRequestData, BaseRequestData
4
+
5
+
6
+ class HttpxRequest(BaseRequestData):
7
+ _instance_of = httpx.Request
8
+
9
+ def body(self):
10
+ data = self._request.read()
11
+ try:
12
+ return data.decode()
13
+ except UnicodeDecodeError:
14
+ pass
15
+ return data
16
+
17
+
18
+ class AsyncHttpxRequest(AsyncBaseRequestData, HttpxRequest):
19
+ _instance_of = httpx.Request
20
+
21
+ async def body(self):
22
+ data = await self._request.aread()
23
+ try:
24
+ return data.decode()
25
+ except UnicodeDecodeError:
26
+ pass
27
+ return data
@@ -0,0 +1,16 @@
1
+ import requests
2
+
3
+ from curlify3._base import BaseRequestData
4
+
5
+
6
+ class RequestsRequest(BaseRequestData):
7
+ _instance_of = requests.PreparedRequest
8
+
9
+ def body(self):
10
+ body = self._request.body
11
+ if isinstance(body, bytes):
12
+ try:
13
+ return body.decode()
14
+ except UnicodeDecodeError:
15
+ pass
16
+ return body
@@ -0,0 +1,20 @@
1
+ from typing import Optional, Union
2
+
3
+ import starlette
4
+
5
+ from starlette.requests import Request
6
+
7
+ from curlify3._base import BaseRequestData
8
+
9
+
10
+ class StarletteRequest(BaseRequestData):
11
+ _instance_of = starlette.requests.Request
12
+
13
+ async def body(self) -> Optional[Union[bytes, str]]:
14
+ self._request: starlette.requests.Request
15
+ data = await self._request.body()
16
+ try:
17
+ return data.decode()
18
+ except UnicodeDecodeError:
19
+ pass
20
+ return data
@@ -0,0 +1,50 @@
1
+ from contextlib import suppress
2
+
3
+ _REQUEST_DATA_CLASSES = []
4
+ _REQUEST_DATA_CLASSES_ASYNC = []
5
+
6
+
7
+ with suppress(ImportError):
8
+ from curlify3._req_requests import RequestsRequest
9
+
10
+ _REQUEST_DATA_CLASSES.append(RequestsRequest)
11
+
12
+
13
+ with suppress(ImportError):
14
+ from curlify3._req_httpx import HttpxRequest
15
+
16
+ _REQUEST_DATA_CLASSES.append(HttpxRequest)
17
+
18
+
19
+ with suppress(ImportError):
20
+ from curlify3._req_httpx import AsyncHttpxRequest
21
+
22
+ _REQUEST_DATA_CLASSES_ASYNC.append(AsyncHttpxRequest)
23
+
24
+
25
+ with suppress(ImportError):
26
+ from curlify3._req_starlette import StarletteRequest
27
+
28
+ _REQUEST_DATA_CLASSES_ASYNC.append(StarletteRequest)
29
+
30
+
31
+ def _find_request_data_obj(request, request_data_classes):
32
+ obj = None
33
+ for _cls in request_data_classes:
34
+ try:
35
+ obj = _cls(request)
36
+ except ValueError:
37
+ continue
38
+ if obj is None:
39
+ raise ValueError('unknown request object')
40
+ return obj
41
+
42
+
43
+ def make_request_obj(request):
44
+ return _find_request_data_obj(request, _REQUEST_DATA_CLASSES)
45
+
46
+
47
+ def make_request_obj_async(request):
48
+ if data_obj := _find_request_data_obj(request, _REQUEST_DATA_CLASSES_ASYNC):
49
+ return data_obj
50
+ return _find_request_data_obj(request, _REQUEST_DATA_CLASSES)
@@ -0,0 +1,34 @@
1
+ [tool.poetry]
2
+ name = "curlify3"
3
+ version = "0.1rc1"
4
+ description = "yet another python Request objects curlificator"
5
+ authors = ["A.Shpak <aleksander.shpak.als@gmail.com>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = ">=3.9,<4.0"
10
+
11
+ [tool.poetry.group.dev.dependencies]
12
+ black = "^24.10.0"
13
+ requests = "^2.32.3"
14
+ httpx = "^0.27.2"
15
+ fastapi = "^0.115.4"
16
+ isort = "^5.13.2"
17
+
18
+ [tool.poetry.group.tests.dependencies]
19
+ pytest = "^8.3.3"
20
+ pytest-asyncio = "^0.24.0"
21
+ pytest-httpx = "^0.33.0"
22
+
23
+ [build-system]
24
+ requires = ["poetry-core"]
25
+ build-backend = "poetry.core.masonry.api"
26
+
27
+ [tool.isort]
28
+ line_length = 120
29
+ include_trailing_comma = true
30
+ lines_between_types = 1
31
+
32
+ [tool.black]
33
+ line-length = 120
34
+ skip-string-normalization = true