uncurl-httpx 0.1.0__tar.gz → 0.1.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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uncurl-httpx
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: uncurl for httpx, fork from uncurl-requests
5
5
  Author-email: cyliu <liucy1983@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/liucy1983/uncurl-httpx
7
7
  Requires-Python: >=3.12
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: httpx>=0.28.0
10
+ Requires-Dist: httpx>=0.28.1
11
11
  Requires-Dist: pyperclip>=1.9.0
12
12
  Provides-Extra: dev
13
13
  Requires-Dist: pytest>=9.0.3; extra == "dev"
@@ -59,7 +59,10 @@ The underlying API:
59
59
  ```python
60
60
  import uncurl
61
61
 
62
- print(uncurl.parse("curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"))
62
+ parsed = uncurl.parse(
63
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
64
+ )
65
+ print(parsed.headers)
63
66
  ```
64
67
 
65
68
  如果你想直接发起请求,也可以调用:
@@ -74,24 +77,54 @@ response = uncurl.request(
74
77
  print(response.status_code)
75
78
  ```
76
79
 
77
- prints the string
80
+ If you want the generated `httpx` code string, ask explicitly:
81
+
82
+ ```python
83
+ import uncurl
84
+
85
+ print(
86
+ uncurl.parse(
87
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'",
88
+ as_object=False,
89
+ )
90
+ )
91
+ ```
92
+
93
+ This prints:
78
94
 
79
95
  ```bash
80
- 'httpx.get("https://pypi.python.org/pypi/uncurl-httpx", headers={
96
+ httpx.get("https://pypi.python.org/pypi/uncurl-httpx", headers={
81
97
  "Accept-Encoding": "gzip,deflate,sdch",
82
- })'
98
+ })
83
99
  ```
84
100
 
85
101
  You can also retrieve the components as python objects:
86
102
 
87
103
  ```python
88
- >>> import uncurl
89
- >>> context = uncurl.parse_context("curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'")
90
- >>> context.url
91
- https://pypi.python.org/pypi/uncurl-httpx
92
- >>> context.headers
93
- OrderedDict([('Accept-Encoding', 'gzip,deflate,sdch')])
104
+ import uncurl
105
+
106
+ context = uncurl.parse_context(
107
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
108
+ )
109
+ print(context.url)
110
+ print(context.headers)
111
+ ```
112
+
113
+ `parse()` already returns a mutable object by default, so you can tweak and send it directly:
114
+
115
+ ```python
116
+ import uncurl
117
+
118
+ parsed = uncurl.parse(
119
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
120
+ )
121
+ parsed.url = "https://example.com/api"
122
+ parsed.headers["X-Debug"] = "1"
123
+
124
+ response = parsed.request(timeout=5.0)
125
+ print(response.status_code)
94
126
  ```
127
+
95
128
  On Mac OS, you can also pipe input to uncurl:
96
129
 
97
130
  ```bash
@@ -101,5 +134,57 @@ pbpaste | uncurl
101
134
  ## Install
102
135
 
103
136
  ```console
104
- $ pip install uncurl
137
+ $ pip install uncurl-httpx
138
+ ```
139
+
140
+ ## Release to PyPI with GitHub Actions
141
+
142
+ This repository includes `.github/workflows/publish.yml` for automated releases. However, if GitHub Actions is disabled on your account, you can publish manually.
143
+
144
+ ## Manual Release to PyPI
145
+
146
+ Create a local `.env` file at the project root:
147
+
148
+ ```dotenv
149
+ PYPI_TOKEN=pypi-your-token-from-pypi-org
150
+ ```
151
+
152
+ Then run:
153
+
154
+ ```bash
155
+ make publish
156
+ ```
157
+
158
+ Or directly:
159
+
160
+ ```bash
161
+ uv run python scripts/publish.py
162
+ ```
163
+
164
+ The script will:
165
+ 1. Run tests
166
+ 2. Build distributions
167
+ 3. Check with `twine`
168
+ 4. Upload to PyPI
169
+
170
+ After upload completes, the new version will appear on [PyPI](https://pypi.org/project/uncurl-httpx/).
171
+
172
+ ## Automated Release with GitHub Actions
173
+
174
+ This repository includes `.github/workflows/publish.yml`.
175
+ Pushing a tag like `v0.1.1` will:
176
+
177
+ 1. run the test suite,
178
+ 2. build the package,
179
+ 3. validate the built artifacts with `twine check`,
180
+ 4. publish to PyPI.
181
+
182
+ Before using it, configure a PyPI Trusted Publisher for this GitHub repository and workflow.
183
+
184
+ Create and push a release tag with:
185
+
186
+ ```bash
187
+ git tag v0.1.1
188
+ git push origin v0.1.1
105
189
  ```
190
+
@@ -42,7 +42,10 @@ The underlying API:
42
42
  ```python
43
43
  import uncurl
44
44
 
45
- print(uncurl.parse("curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"))
45
+ parsed = uncurl.parse(
46
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
47
+ )
48
+ print(parsed.headers)
46
49
  ```
47
50
 
48
51
  如果你想直接发起请求,也可以调用:
@@ -57,24 +60,54 @@ response = uncurl.request(
57
60
  print(response.status_code)
58
61
  ```
59
62
 
60
- prints the string
63
+ If you want the generated `httpx` code string, ask explicitly:
64
+
65
+ ```python
66
+ import uncurl
67
+
68
+ print(
69
+ uncurl.parse(
70
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'",
71
+ as_object=False,
72
+ )
73
+ )
74
+ ```
75
+
76
+ This prints:
61
77
 
62
78
  ```bash
63
- 'httpx.get("https://pypi.python.org/pypi/uncurl-httpx", headers={
79
+ httpx.get("https://pypi.python.org/pypi/uncurl-httpx", headers={
64
80
  "Accept-Encoding": "gzip,deflate,sdch",
65
- })'
81
+ })
66
82
  ```
67
83
 
68
84
  You can also retrieve the components as python objects:
69
85
 
70
86
  ```python
71
- >>> import uncurl
72
- >>> context = uncurl.parse_context("curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'")
73
- >>> context.url
74
- https://pypi.python.org/pypi/uncurl-httpx
75
- >>> context.headers
76
- OrderedDict([('Accept-Encoding', 'gzip,deflate,sdch')])
87
+ import uncurl
88
+
89
+ context = uncurl.parse_context(
90
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
91
+ )
92
+ print(context.url)
93
+ print(context.headers)
94
+ ```
95
+
96
+ `parse()` already returns a mutable object by default, so you can tweak and send it directly:
97
+
98
+ ```python
99
+ import uncurl
100
+
101
+ parsed = uncurl.parse(
102
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
103
+ )
104
+ parsed.url = "https://example.com/api"
105
+ parsed.headers["X-Debug"] = "1"
106
+
107
+ response = parsed.request(timeout=5.0)
108
+ print(response.status_code)
77
109
  ```
110
+
78
111
  On Mac OS, you can also pipe input to uncurl:
79
112
 
80
113
  ```bash
@@ -84,5 +117,57 @@ pbpaste | uncurl
84
117
  ## Install
85
118
 
86
119
  ```console
87
- $ pip install uncurl
120
+ $ pip install uncurl-httpx
121
+ ```
122
+
123
+ ## Release to PyPI with GitHub Actions
124
+
125
+ This repository includes `.github/workflows/publish.yml` for automated releases. However, if GitHub Actions is disabled on your account, you can publish manually.
126
+
127
+ ## Manual Release to PyPI
128
+
129
+ Create a local `.env` file at the project root:
130
+
131
+ ```dotenv
132
+ PYPI_TOKEN=pypi-your-token-from-pypi-org
133
+ ```
134
+
135
+ Then run:
136
+
137
+ ```bash
138
+ make publish
139
+ ```
140
+
141
+ Or directly:
142
+
143
+ ```bash
144
+ uv run python scripts/publish.py
145
+ ```
146
+
147
+ The script will:
148
+ 1. Run tests
149
+ 2. Build distributions
150
+ 3. Check with `twine`
151
+ 4. Upload to PyPI
152
+
153
+ After upload completes, the new version will appear on [PyPI](https://pypi.org/project/uncurl-httpx/).
154
+
155
+ ## Automated Release with GitHub Actions
156
+
157
+ This repository includes `.github/workflows/publish.yml`.
158
+ Pushing a tag like `v0.1.1` will:
159
+
160
+ 1. run the test suite,
161
+ 2. build the package,
162
+ 3. validate the built artifacts with `twine check`,
163
+ 4. publish to PyPI.
164
+
165
+ Before using it, configure a PyPI Trusted Publisher for this GitHub repository and workflow.
166
+
167
+ Create and push a release tag with:
168
+
169
+ ```bash
170
+ git tag v0.1.1
171
+ git push origin v0.1.1
88
172
  ```
173
+
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "uncurl-httpx"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "uncurl for httpx, fork from uncurl-requests"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -12,7 +12,7 @@ authors = [
12
12
  { name = "cyliu", email = "liucy1983@gmail.com" },
13
13
  ]
14
14
  dependencies = [
15
- "httpx>=0.28.0",
15
+ "httpx>=0.28.1",
16
16
  "pyperclip>=1.9.0",
17
17
  ]
18
18
 
@@ -3,7 +3,7 @@ from unittest.mock import sentinel
3
3
 
4
4
 
5
5
  def assert_parse(command, expected, **kwargs):
6
- assert uncurl.parse(command, **kwargs) == expected
6
+ assert uncurl.parse(command, as_object=False, **kwargs) == expected
7
7
 
8
8
 
9
9
  def test_request_executes_httpx_request(mocker=None):
@@ -30,6 +30,41 @@ def test_request_executes_httpx_request(mocker=None):
30
30
  )
31
31
 
32
32
 
33
+ def test_parse_can_return_mutable_object():
34
+ parsed = uncurl.parse("curl 'https://pypi.python.org/pypi/uncurl-httpx'", as_object=True)
35
+
36
+ assert isinstance(parsed, uncurl.ParsedContext)
37
+ parsed.method = 'post'
38
+ parsed.data = 'patched'
39
+
40
+ assert parsed.to_code() == """httpx.post(\"https://pypi.python.org/pypi/uncurl-httpx\",
41
+ data='patched',
42
+ )"""
43
+
44
+
45
+ def test_parse_object_can_request():
46
+ from unittest.mock import patch
47
+
48
+ parsed = uncurl.parse(
49
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'",
50
+ as_object=True,
51
+ )
52
+ parsed.url = 'https://example.com/override'
53
+
54
+ with patch("uncurl.api.httpx.request", return_value=sentinel.response) as request_mock:
55
+ response = parsed.request(timeout=1.0)
56
+
57
+ assert response is sentinel.response
58
+ request_mock.assert_called_once_with(
59
+ 'get',
60
+ 'https://example.com/override',
61
+ timeout=1.0,
62
+ headers={
63
+ 'Accept-Encoding': 'gzip,deflate,sdch',
64
+ },
65
+ )
66
+
67
+
33
68
  def test_basic_get():
34
69
  assert_parse("curl 'https://pypi.python.org/pypi/uncurl-httpx'", """httpx.get("https://pypi.python.org/pypi/uncurl-httpx")"""
35
70
  )
@@ -0,0 +1,3 @@
1
+ from .api import ParsedContext, parse, parse_context, request
2
+
3
+ __all__ = ("ParsedContext", "parse", "parse_context", "request")
@@ -3,8 +3,10 @@ import argparse
3
3
  import json
4
4
  import re
5
5
  import shlex
6
- from collections import OrderedDict, namedtuple
6
+ from collections import OrderedDict
7
+ from dataclasses import dataclass, field
7
8
  from http.cookies import SimpleCookie
9
+ from typing import Any, Optional, Tuple
8
10
 
9
11
  import httpx
10
12
 
@@ -26,7 +28,85 @@ parser.add_argument('-U', '--proxy-user', default='')
26
28
 
27
29
  BASE_INDENT = " " * 4
28
30
 
29
- ParsedContext = namedtuple('ParsedContext', ['method', 'url', 'data', 'headers', 'cookies', 'verify', 'auth', 'proxy'])
31
+
32
+ @dataclass
33
+ class ParsedContext:
34
+ method: str
35
+ url: str
36
+ data: Optional[str] = None
37
+ headers: OrderedDict = field(default_factory=OrderedDict)
38
+ cookies: OrderedDict = field(default_factory=OrderedDict)
39
+ verify: bool = False
40
+ auth: Tuple[str, ...] = ()
41
+ proxy: Any = field(default_factory=dict)
42
+
43
+ def build_request_kwargs(self, **kwargs):
44
+ request_kwargs = dict(kwargs)
45
+ if 'allow_redirects' in request_kwargs:
46
+ request_kwargs['follow_redirects'] = request_kwargs.pop('allow_redirects')
47
+
48
+ if self.data:
49
+ request_kwargs['data'] = self.data
50
+
51
+ if self.headers:
52
+ request_kwargs['headers'] = dict(self.headers)
53
+
54
+ if self.cookies:
55
+ request_kwargs['cookies'] = dict(self.cookies)
56
+
57
+ if self.auth:
58
+ request_kwargs['auth'] = self.auth
59
+
60
+ if self.proxy:
61
+ request_kwargs['proxy'] = self.proxy
62
+
63
+ if self.verify:
64
+ request_kwargs['verify'] = False
65
+
66
+ return request_kwargs
67
+
68
+ def to_code(self, **kwargs):
69
+ request_kwargs = []
70
+
71
+ for key, value in sorted(kwargs.items()):
72
+ if key == 'allow_redirects':
73
+ key = 'follow_redirects'
74
+ request_kwargs.append(format_request_arg(key, value))
75
+
76
+ if self.data:
77
+ request_kwargs.append(format_request_arg('data', self.data))
78
+
79
+ if self.headers:
80
+ request_kwargs.append(format_dict_arg('headers', self.headers))
81
+
82
+ if self.cookies:
83
+ request_kwargs.append(format_dict_arg('cookies', self.cookies))
84
+
85
+ if self.auth:
86
+ request_kwargs.append(format_request_arg('auth', self.auth))
87
+
88
+ if self.proxy:
89
+ request_kwargs.append(format_request_arg('proxy', self.proxy))
90
+
91
+ if self.verify:
92
+ request_kwargs.append(format_request_arg('verify', False, trailing_comma=False))
93
+
94
+ if not request_kwargs:
95
+ return "httpx.{method}({url})".format(
96
+ method=self.method,
97
+ url=json.dumps(self.url),
98
+ )
99
+
100
+ return """httpx.{method}({url},
101
+ {request_kwargs}
102
+ )""".format(
103
+ method=self.method,
104
+ url=json.dumps(self.url),
105
+ request_kwargs='\n'.join(request_kwargs),
106
+ )
107
+
108
+ def request(self, **kwargs):
109
+ return httpx.request(self.method, self.url, **self.build_request_kwargs(**kwargs))
30
110
 
31
111
 
32
112
  def normalize_newlines(multiline_text):
@@ -95,74 +175,18 @@ def parse_context(curl_command):
95
175
  )
96
176
 
97
177
 
98
- def parse(curl_command, **kargs):
178
+ def parse(curl_command, as_object=True, **kwargs):
99
179
  parsed_context = parse_context(curl_command)
100
180
 
101
- request_kargs = []
102
- for key, value in sorted(kargs.items()):
103
- if key == 'allow_redirects':
104
- key = 'follow_redirects'
105
- request_kargs.append(format_request_arg(key, value))
106
-
107
- if parsed_context.data:
108
- request_kargs.append(format_request_arg('data', parsed_context.data))
109
-
110
- if parsed_context.headers:
111
- request_kargs.append(format_dict_arg('headers', parsed_context.headers))
112
-
113
- if parsed_context.cookies:
114
- request_kargs.append(format_dict_arg('cookies', parsed_context.cookies))
115
-
116
- if parsed_context.auth:
117
- request_kargs.append(format_request_arg('auth', parsed_context.auth))
118
-
119
- if parsed_context.proxy:
120
- request_kargs.append(format_request_arg('proxy', parsed_context.proxy))
121
-
122
- if parsed_context.verify:
123
- request_kargs.append(format_request_arg('verify', False, trailing_comma=False))
124
-
125
- if not request_kargs:
126
- return "httpx.{method}({url})".format(
127
- method=parsed_context.method,
128
- url=json.dumps(parsed_context.url),
129
- )
181
+ if as_object:
182
+ return parsed_context
130
183
 
131
- return """httpx.{method}({url},
132
- {request_kargs}
133
- )""".format(
134
- method=parsed_context.method,
135
- url=json.dumps(parsed_context.url),
136
- request_kargs='\n'.join(request_kargs),
137
- )
184
+ return parsed_context.to_code(**kwargs)
138
185
 
139
186
 
140
187
  def request(curl_command, **kwargs):
141
188
  parsed_context = parse_context(curl_command)
142
-
143
- request_kwargs = dict(kwargs)
144
- if 'allow_redirects' in request_kwargs:
145
- request_kwargs['follow_redirects'] = request_kwargs.pop('allow_redirects')
146
-
147
- if parsed_context.data:
148
- request_kwargs['data'] = parsed_context.data
149
-
150
- if parsed_context.headers:
151
- request_kwargs['headers'] = dict(parsed_context.headers)
152
-
153
- if parsed_context.cookies:
154
- request_kwargs['cookies'] = dict(parsed_context.cookies)
155
-
156
- if parsed_context.auth:
157
- request_kwargs['auth'] = parsed_context.auth
158
-
159
- if parsed_context.proxy:
160
- request_kwargs['proxy'] = parsed_context.proxy
161
-
162
- if parsed_context.verify:
163
- request_kwargs['verify'] = False
164
-
165
- return httpx.request(parsed_context.method, parsed_context.url, **request_kwargs)
189
+ return parsed_context.request(**kwargs)
166
190
 
167
191
 
168
192
  def format_request_arg(key, value, trailing_comma=True):
@@ -10,10 +10,10 @@ def main():
10
10
  if sys.stdin.isatty():
11
11
  if len(sys.argv) > 1:
12
12
  # If an argument is passed
13
- result = parse(sys.argv[1])
13
+ result = parse(sys.argv[1], as_object=False)
14
14
  else:
15
15
  # Otherwise pull from clipboard
16
- result = parse(pyperclip.paste())
16
+ result = parse(pyperclip.paste(), as_object=False)
17
17
  else:
18
- result = parse(sys.stdin.read())
18
+ result = parse(sys.stdin.read(), as_object=False)
19
19
  print("\n" + result)
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uncurl-httpx
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: uncurl for httpx, fork from uncurl-requests
5
5
  Author-email: cyliu <liucy1983@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/liucy1983/uncurl-httpx
7
7
  Requires-Python: >=3.12
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: httpx>=0.28.0
10
+ Requires-Dist: httpx>=0.28.1
11
11
  Requires-Dist: pyperclip>=1.9.0
12
12
  Provides-Extra: dev
13
13
  Requires-Dist: pytest>=9.0.3; extra == "dev"
@@ -59,7 +59,10 @@ The underlying API:
59
59
  ```python
60
60
  import uncurl
61
61
 
62
- print(uncurl.parse("curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"))
62
+ parsed = uncurl.parse(
63
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
64
+ )
65
+ print(parsed.headers)
63
66
  ```
64
67
 
65
68
  如果你想直接发起请求,也可以调用:
@@ -74,24 +77,54 @@ response = uncurl.request(
74
77
  print(response.status_code)
75
78
  ```
76
79
 
77
- prints the string
80
+ If you want the generated `httpx` code string, ask explicitly:
81
+
82
+ ```python
83
+ import uncurl
84
+
85
+ print(
86
+ uncurl.parse(
87
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'",
88
+ as_object=False,
89
+ )
90
+ )
91
+ ```
92
+
93
+ This prints:
78
94
 
79
95
  ```bash
80
- 'httpx.get("https://pypi.python.org/pypi/uncurl-httpx", headers={
96
+ httpx.get("https://pypi.python.org/pypi/uncurl-httpx", headers={
81
97
  "Accept-Encoding": "gzip,deflate,sdch",
82
- })'
98
+ })
83
99
  ```
84
100
 
85
101
  You can also retrieve the components as python objects:
86
102
 
87
103
  ```python
88
- >>> import uncurl
89
- >>> context = uncurl.parse_context("curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'")
90
- >>> context.url
91
- https://pypi.python.org/pypi/uncurl-httpx
92
- >>> context.headers
93
- OrderedDict([('Accept-Encoding', 'gzip,deflate,sdch')])
104
+ import uncurl
105
+
106
+ context = uncurl.parse_context(
107
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
108
+ )
109
+ print(context.url)
110
+ print(context.headers)
111
+ ```
112
+
113
+ `parse()` already returns a mutable object by default, so you can tweak and send it directly:
114
+
115
+ ```python
116
+ import uncurl
117
+
118
+ parsed = uncurl.parse(
119
+ "curl 'https://pypi.python.org/pypi/uncurl-httpx' -H 'Accept-Encoding: gzip,deflate,sdch'"
120
+ )
121
+ parsed.url = "https://example.com/api"
122
+ parsed.headers["X-Debug"] = "1"
123
+
124
+ response = parsed.request(timeout=5.0)
125
+ print(response.status_code)
94
126
  ```
127
+
95
128
  On Mac OS, you can also pipe input to uncurl:
96
129
 
97
130
  ```bash
@@ -101,5 +134,57 @@ pbpaste | uncurl
101
134
  ## Install
102
135
 
103
136
  ```console
104
- $ pip install uncurl
137
+ $ pip install uncurl-httpx
138
+ ```
139
+
140
+ ## Release to PyPI with GitHub Actions
141
+
142
+ This repository includes `.github/workflows/publish.yml` for automated releases. However, if GitHub Actions is disabled on your account, you can publish manually.
143
+
144
+ ## Manual Release to PyPI
145
+
146
+ Create a local `.env` file at the project root:
147
+
148
+ ```dotenv
149
+ PYPI_TOKEN=pypi-your-token-from-pypi-org
150
+ ```
151
+
152
+ Then run:
153
+
154
+ ```bash
155
+ make publish
156
+ ```
157
+
158
+ Or directly:
159
+
160
+ ```bash
161
+ uv run python scripts/publish.py
162
+ ```
163
+
164
+ The script will:
165
+ 1. Run tests
166
+ 2. Build distributions
167
+ 3. Check with `twine`
168
+ 4. Upload to PyPI
169
+
170
+ After upload completes, the new version will appear on [PyPI](https://pypi.org/project/uncurl-httpx/).
171
+
172
+ ## Automated Release with GitHub Actions
173
+
174
+ This repository includes `.github/workflows/publish.yml`.
175
+ Pushing a tag like `v0.1.1` will:
176
+
177
+ 1. run the test suite,
178
+ 2. build the package,
179
+ 3. validate the built artifacts with `twine check`,
180
+ 4. publish to PyPI.
181
+
182
+ Before using it, configure a PyPI Trusted Publisher for this GitHub repository and workflow.
183
+
184
+ Create and push a release tag with:
185
+
186
+ ```bash
187
+ git tag v0.1.1
188
+ git push origin v0.1.1
105
189
  ```
190
+
@@ -1,4 +1,4 @@
1
- httpx>=0.28.0
1
+ httpx>=0.28.1
2
2
  pyperclip>=1.9.0
3
3
 
4
4
  [dev]
@@ -1,3 +0,0 @@
1
- from .api import parse, parse_context, request
2
-
3
- __all__ = ("parse", "parse_context", "request")
File without changes
File without changes
File without changes