curlify3 0.1__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.
curlify3-0.1/PKG-INFO ADDED
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.1
2
+ Name: curlify3
3
+ Version: 0.1
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 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
+ ### Support request's objects of:
22
+
23
+ - requests
24
+ - httpx
25
+ - starlette/fastapi
26
+
27
+ ## Installation
28
+
29
+ ```sh
30
+ pip install curlify3
31
+ ```
32
+
33
+ ## Example
34
+
35
+ ```py
36
+ from curlify3 import to_curl
37
+ import requests
38
+
39
+ response = requests.get("http://google.ru")
40
+ print(to_curl(response.request))
41
+ # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' -H 'accept: */*' -H 'connection: keep-alive' http://www.google.ru/
42
+ ```
43
+
curlify3-0.1/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # yet another library to convert 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
+ ### Support request's objects of:
7
+
8
+ - requests
9
+ - httpx
10
+ - starlette/fastapi
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ pip install curlify3
16
+ ```
17
+
18
+ ## Example
19
+
20
+ ```py
21
+ from curlify3 import to_curl
22
+ import requests
23
+
24
+ response = requests.get("http://google.ru")
25
+ print(to_curl(response.request))
26
+ # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' -H 'accept: */*' -H 'connection: keep-alive' http://www.google.ru/
27
+ ```
@@ -0,0 +1,7 @@
1
+ from curlify3._curl import to_curl, to_curl_async
2
+
3
+ __version__ = "0.1"
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.1"
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