tldextract 5.1.1__tar.gz → 5.1.3__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.
Files changed (40) hide show
  1. {tldextract-5.1.1 → tldextract-5.1.3}/.github/workflows/ci.yml +16 -9
  2. {tldextract-5.1.1 → tldextract-5.1.3}/CHANGELOG.md +24 -0
  3. {tldextract-5.1.1 → tldextract-5.1.3}/LICENSE +1 -1
  4. {tldextract-5.1.1 → tldextract-5.1.3}/PKG-INFO +31 -18
  5. {tldextract-5.1.1 → tldextract-5.1.3}/README.md +23 -14
  6. {tldextract-5.1.1 → tldextract-5.1.3}/pyproject.toml +12 -6
  7. tldextract-5.1.3/scripts/release.py +236 -0
  8. tldextract-5.1.3/tests/__snapshots__/test_release.ambr +247 -0
  9. {tldextract-5.1.1 → tldextract-5.1.3}/tests/main_test.py +16 -14
  10. {tldextract-5.1.1 → tldextract-5.1.3}/tests/test_cache.py +4 -3
  11. {tldextract-5.1.1 → tldextract-5.1.3}/tests/test_parallel.py +1 -13
  12. tldextract-5.1.3/tests/test_release.py +96 -0
  13. {tldextract-5.1.1 → tldextract-5.1.3}/tests/test_trie.py +1 -0
  14. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/.tld_set_snapshot +4390 -2405
  15. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/__main__.py +0 -1
  16. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/_version.py +2 -2
  17. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/cache.py +8 -20
  18. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/remote.py +6 -28
  19. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/suffix_list.py +3 -1
  20. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/tldextract.py +16 -22
  21. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract.egg-info/PKG-INFO +31 -18
  22. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract.egg-info/SOURCES.txt +3 -0
  23. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract.egg-info/requires.txt +6 -1
  24. tldextract-5.1.3/tox.ini +22 -0
  25. tldextract-5.1.1/tox.ini +0 -22
  26. {tldextract-5.1.1 → tldextract-5.1.3}/.github/FUNDING.yml +0 -0
  27. {tldextract-5.1.1 → tldextract-5.1.3}/.gitignore +0 -0
  28. {tldextract-5.1.1 → tldextract-5.1.3}/setup.cfg +0 -0
  29. {tldextract-5.1.1 → tldextract-5.1.3}/tests/__init__.py +0 -0
  30. {tldextract-5.1.1 → tldextract-5.1.3}/tests/cli_test.py +0 -0
  31. {tldextract-5.1.1 → tldextract-5.1.3}/tests/conftest.py +0 -0
  32. {tldextract-5.1.1 → tldextract-5.1.3}/tests/custom_suffix_test.py +0 -0
  33. {tldextract-5.1.1 → tldextract-5.1.3}/tests/fixtures/fake_suffix_list_fixture.dat +0 -0
  34. {tldextract-5.1.1 → tldextract-5.1.3}/tests/integration_test.py +0 -0
  35. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/__init__.py +0 -0
  36. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/cli.py +0 -0
  37. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract/py.typed +0 -0
  38. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract.egg-info/dependency_links.txt +0 -0
  39. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract.egg-info/entry_points.txt +0 -0
  40. {tldextract-5.1.1 → tldextract-5.1.3}/tldextract.egg-info/top_level.txt +0 -0
@@ -1,5 +1,11 @@
1
1
  name: build
2
- on: [push, pull_request]
2
+ on:
3
+ pull_request: {}
4
+ push:
5
+ branches:
6
+ - "master"
7
+ tags-ignore:
8
+ - "**"
3
9
  jobs:
4
10
  test:
5
11
  strategy:
@@ -8,32 +14,33 @@ jobs:
8
14
  os: [macos-latest, windows-latest, ubuntu-latest]
9
15
  language:
10
16
  [
11
- {python-version: "3.8", toxenv: "py38"},
12
17
  {python-version: "3.9", toxenv: "py39"},
13
18
  {python-version: "3.10", toxenv: "py310"},
14
19
  {python-version: "3.11", toxenv: "py311"},
15
20
  {python-version: "3.12", toxenv: "py312"},
16
- {python-version: "pypy3.8", toxenv: "pypy38"},
21
+ {python-version: "3.13", toxenv: "py313"},
22
+ {python-version: "pypy3.9", toxenv: "pypy39"},
23
+ {python-version: "pypy3.10", toxenv: "pypy310"},
17
24
  ]
18
25
  include:
19
26
  - os: ubuntu-latest
20
- language: {python-version: "3.8", toxenv: "codestyle"}
27
+ language: {python-version: "3.9", toxenv: "codestyle"}
21
28
  - os: ubuntu-latest
22
- language: {python-version: "3.8", toxenv: "lint"}
29
+ language: {python-version: "3.9", toxenv: "lint"}
23
30
  - os: ubuntu-latest
24
- language: {python-version: "3.8", toxenv: "typecheck"}
31
+ language: {python-version: "3.9", toxenv: "typecheck"}
25
32
  runs-on: ${{ matrix.os }}
26
33
  steps:
27
34
  - name: Check out repository
28
35
  uses: actions/checkout@v4
29
36
  - name: Setup Python
30
- uses: actions/setup-python@v4
37
+ uses: actions/setup-python@v5
31
38
  with:
32
39
  python-version: ${{ matrix.language.python-version }}
40
+ check-latest: true
33
41
  - name: Install Python requirements
34
42
  run: |
35
- pip install --upgrade pip
36
- pip install --upgrade --editable '.[testing]'
43
+ pip install --upgrade tox tox-uv
37
44
  - name: Test
38
45
  run: tox
39
46
  env:
@@ -3,6 +3,30 @@
3
3
  After upgrading, update your cache file by deleting it or via `tldextract
4
4
  --update`.
5
5
 
6
+ ## 5.1.3 (2024-11-04)
7
+
8
+ * Bugfixes
9
+ * Reduce logging errors ([`921a825`](https://github.com/john-kurkowski/tldextract/commit/921a82523c0e4403d21d50b2c3410d9af43520ac))
10
+ * Drop support for EOL Python 3.8 ([#340](https://github.com/john-kurkowski/tldextract/issues/340))
11
+ * Support Python 3.13 ([#341](https://github.com/john-kurkowski/tldextract/issues/341))
12
+ * Update bundled snapshot
13
+ * Documentation
14
+ * Clarify how to use your own definitions
15
+ * Clarify first-successful definitions vs. merged definitions
16
+ * Misc.
17
+ * Switch from Black to Ruff ([#333](https://github.com/john-kurkowski/tldextract/issues/333))
18
+ * Switch from pip to uv, during tox ([#324](https://github.com/john-kurkowski/tldextract/issues/324))
19
+
20
+ ## 5.1.2 (2024-03-18)
21
+
22
+ * Bugfixes
23
+ * Remove `socket.inet_pton`, to fix platform-dependent IP parsing ([#318](https://github.com/john-kurkowski/tldextract/issues/318))
24
+ * Use non-capturing groups for IPv4 address detection, for a slight speed boost ([#323](https://github.com/john-kurkowski/tldextract/issues/323))
25
+ * Misc.
26
+ * Add CI for PyPy3.9 and PyPy3.10 ([#316](https://github.com/john-kurkowski/tldextract/issues/316))
27
+ * Add script to automate package release process ([#325](https://github.com/john-kurkowski/tldextract/issues/325))
28
+ * Update LICENSE copyright years
29
+
6
30
  ## 5.1.1 (2023-11-16)
7
31
 
8
32
  * Bugfixes
@@ -1,6 +1,6 @@
1
1
  BSD 3-Clause License
2
2
 
3
- Copyright (c) 2020, John Kurkowski
3
+ Copyright (c) 2013-2024, John Kurkowski
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tldextract
3
- Version: 5.1.1
3
+ Version: 5.1.3
4
4
  Summary: Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well.
5
5
  Author-email: John Kurkowski <john.kurkowski@gmail.com>
6
6
  License: BSD-3-Clause
@@ -10,27 +10,31 @@ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Topic :: Utilities
11
11
  Classifier: License :: OSI Approved :: BSD License
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
13
  Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
- Requires-Python: >=3.8
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.9
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: idna
22
22
  Requires-Dist: requests>=2.1.0
23
23
  Requires-Dist: requests-file>=1.4
24
24
  Requires-Dist: filelock>=3.0.8
25
+ Provides-Extra: release
26
+ Requires-Dist: build; extra == "release"
27
+ Requires-Dist: twine; extra == "release"
25
28
  Provides-Extra: testing
26
- Requires-Dist: black; extra == "testing"
27
29
  Requires-Dist: mypy; extra == "testing"
28
30
  Requires-Dist: pytest; extra == "testing"
29
31
  Requires-Dist: pytest-gitignore; extra == "testing"
30
32
  Requires-Dist: pytest-mock; extra == "testing"
31
33
  Requires-Dist: responses; extra == "testing"
32
34
  Requires-Dist: ruff; extra == "testing"
35
+ Requires-Dist: syrupy; extra == "testing"
33
36
  Requires-Dist: tox; extra == "testing"
37
+ Requires-Dist: tox-uv; extra == "testing"
34
38
  Requires-Dist: types-filelock; extra == "testing"
35
39
  Requires-Dist: types-requests; extra == "testing"
36
40
 
@@ -125,8 +129,8 @@ tldextract http://forums.bbc.co.uk
125
129
 
126
130
  Beware when first calling `tldextract`, it updates its TLD list with a live HTTP
127
131
  request. This updated TLD set is usually cached indefinitely in `$HOME/.cache/python-tldextract`.
128
- To control the cache's location, set TLDEXTRACT_CACHE environment variable or set the
129
- cache_dir path in TLDExtract initialization.
132
+ To control the cache's location, set the `TLDEXTRACT_CACHE` environment variable or set the
133
+ `cache_dir` path when constructing a `TLDExtract`.
130
134
 
131
135
  (Arguably runtime bootstrapping like that shouldn't be the default behavior,
132
136
  like for production systems. But I want you to have the latest TLDs, especially
@@ -215,10 +219,12 @@ extract = tldextract.TLDExtract(
215
219
  fallback_to_snapshot=False)
216
220
  ```
217
221
 
218
- The above snippet will fetch from the URL *you* specified, upon first need to download the
219
- suffix list (i.e. if the cached version doesn't exist).
222
+ If the cached version of public suffix definitions doesn't exist, such as on
223
+ the first run, the above snippet will request the URLs you specified in order,
224
+ and use the first successful response.
220
225
 
221
- If you want to use input data from your local filesystem, just use the `file://` protocol:
226
+ If you want to use input data from your local filesystem, use the `file://`
227
+ protocol with an absolute path:
222
228
 
223
229
  ```python
224
230
  extract = tldextract.TLDExtract(
@@ -227,17 +233,24 @@ extract = tldextract.TLDExtract(
227
233
  fallback_to_snapshot=False)
228
234
  ```
229
235
 
230
- Use an absolute path when specifying the `suffix_list_urls` keyword argument.
231
- `os.path` is your friend.
232
-
233
- The command line update command can be used with a URL or local file you specify:
236
+ This also works via command line update:
234
237
 
235
238
  ```zsh
236
239
  tldextract --update --suffix_list_url "http://foo.bar.baz"
237
240
  ```
238
241
 
239
- This could be useful in production when you don't want the delay associated with updating the suffix
240
- list on first use, or if you are behind a complex firewall that prevents a simple update from working.
242
+ Using your own URLs could be useful in production when you don't want the delay
243
+ with updating the suffix list on first use, or if you are behind a complex
244
+ firewall.
245
+
246
+ You can also specify additional suffixes in the `extra_suffixes` param. These
247
+ will be merged into whatever public suffix definitions are already in use by
248
+ `tldextract`.
249
+
250
+ ```python
251
+ extract = tldextract.TLDExtract(
252
+ extra_suffixes=["foo", "bar", "baz"])
253
+ ```
241
254
 
242
255
  ## FAQ
243
256
 
@@ -246,9 +259,9 @@ list on first use, or if you are behind a complex firewall that prevents a simpl
246
259
  This project doesn't contain an actual list of public suffixes. That comes from
247
260
  [the Public Suffix List (PSL)](https://publicsuffix.org/). Submit amendments there.
248
261
 
249
- (In the meantime, you can tell tldextract about your exception by either
262
+ In the meantime, you can tell tldextract about your exception by either
250
263
  forking the PSL and using your fork in the `suffix_list_urls` param, or adding
251
- your suffix piecemeal with the `extra_suffixes` param.)
264
+ your suffix piecemeal with the `extra_suffixes` param.
252
265
 
253
266
  ### I see my suffix in [the Public Suffix List (PSL)](https://publicsuffix.org/), but this library doesn't extract it.
254
267
 
@@ -305,5 +318,5 @@ tox -e py311
305
318
  Automatically format all code:
306
319
 
307
320
  ```zsh
308
- black .
321
+ ruff format .
309
322
  ```
@@ -89,8 +89,8 @@ tldextract http://forums.bbc.co.uk
89
89
 
90
90
  Beware when first calling `tldextract`, it updates its TLD list with a live HTTP
91
91
  request. This updated TLD set is usually cached indefinitely in `$HOME/.cache/python-tldextract`.
92
- To control the cache's location, set TLDEXTRACT_CACHE environment variable or set the
93
- cache_dir path in TLDExtract initialization.
92
+ To control the cache's location, set the `TLDEXTRACT_CACHE` environment variable or set the
93
+ `cache_dir` path when constructing a `TLDExtract`.
94
94
 
95
95
  (Arguably runtime bootstrapping like that shouldn't be the default behavior,
96
96
  like for production systems. But I want you to have the latest TLDs, especially
@@ -179,10 +179,12 @@ extract = tldextract.TLDExtract(
179
179
  fallback_to_snapshot=False)
180
180
  ```
181
181
 
182
- The above snippet will fetch from the URL *you* specified, upon first need to download the
183
- suffix list (i.e. if the cached version doesn't exist).
182
+ If the cached version of public suffix definitions doesn't exist, such as on
183
+ the first run, the above snippet will request the URLs you specified in order,
184
+ and use the first successful response.
184
185
 
185
- If you want to use input data from your local filesystem, just use the `file://` protocol:
186
+ If you want to use input data from your local filesystem, use the `file://`
187
+ protocol with an absolute path:
186
188
 
187
189
  ```python
188
190
  extract = tldextract.TLDExtract(
@@ -191,17 +193,24 @@ extract = tldextract.TLDExtract(
191
193
  fallback_to_snapshot=False)
192
194
  ```
193
195
 
194
- Use an absolute path when specifying the `suffix_list_urls` keyword argument.
195
- `os.path` is your friend.
196
-
197
- The command line update command can be used with a URL or local file you specify:
196
+ This also works via command line update:
198
197
 
199
198
  ```zsh
200
199
  tldextract --update --suffix_list_url "http://foo.bar.baz"
201
200
  ```
202
201
 
203
- This could be useful in production when you don't want the delay associated with updating the suffix
204
- list on first use, or if you are behind a complex firewall that prevents a simple update from working.
202
+ Using your own URLs could be useful in production when you don't want the delay
203
+ with updating the suffix list on first use, or if you are behind a complex
204
+ firewall.
205
+
206
+ You can also specify additional suffixes in the `extra_suffixes` param. These
207
+ will be merged into whatever public suffix definitions are already in use by
208
+ `tldextract`.
209
+
210
+ ```python
211
+ extract = tldextract.TLDExtract(
212
+ extra_suffixes=["foo", "bar", "baz"])
213
+ ```
205
214
 
206
215
  ## FAQ
207
216
 
@@ -210,9 +219,9 @@ list on first use, or if you are behind a complex firewall that prevents a simpl
210
219
  This project doesn't contain an actual list of public suffixes. That comes from
211
220
  [the Public Suffix List (PSL)](https://publicsuffix.org/). Submit amendments there.
212
221
 
213
- (In the meantime, you can tell tldextract about your exception by either
222
+ In the meantime, you can tell tldextract about your exception by either
214
223
  forking the PSL and using your fork in the `suffix_list_urls` param, or adding
215
- your suffix piecemeal with the `extra_suffixes` param.)
224
+ your suffix piecemeal with the `extra_suffixes` param.
216
225
 
217
226
  ### I see my suffix in [the Public Suffix List (PSL)](https://publicsuffix.org/), but this library doesn't extract it.
218
227
 
@@ -269,5 +278,5 @@ tox -e py311
269
278
  Automatically format all code:
270
279
 
271
280
  ```zsh
272
- black .
281
+ ruff format .
273
282
  ```
@@ -23,13 +23,13 @@ classifiers = [
23
23
  "Topic :: Utilities",
24
24
  "License :: OSI Approved :: BSD License",
25
25
  "Programming Language :: Python :: 3",
26
- "Programming Language :: Python :: 3.8",
27
26
  "Programming Language :: Python :: 3.9",
28
27
  "Programming Language :: Python :: 3.10",
29
28
  "Programming Language :: Python :: 3.11",
30
29
  "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
31
  ]
32
- requires-python = ">=3.8"
32
+ requires-python = ">=3.9"
33
33
  dynamic = ["version"]
34
34
  readme = "README.md"
35
35
 
@@ -41,15 +41,20 @@ dependencies = [
41
41
  ]
42
42
 
43
43
  [project.optional-dependencies]
44
+ release = [
45
+ "build",
46
+ "twine",
47
+ ]
44
48
  testing = [
45
- "black",
46
49
  "mypy",
47
50
  "pytest",
48
51
  "pytest-gitignore",
49
52
  "pytest-mock",
50
53
  "responses",
51
54
  "ruff",
55
+ "syrupy",
52
56
  "tox",
57
+ "tox-uv",
53
58
  "types-filelock",
54
59
  "types-requests",
55
60
  ]
@@ -79,12 +84,13 @@ write_to = "tldextract/_version.py"
79
84
  version = {attr = "setuptools_scm.get_version"}
80
85
 
81
86
  [tool.mypy]
87
+ explicit_package_bases = true
82
88
  strict = true
83
89
 
84
90
  [tool.pytest.ini_options]
85
91
  addopts = "--doctest-modules"
86
92
 
87
- [tool.ruff]
93
+ [tool.ruff.lint]
88
94
  select = [
89
95
  "A",
90
96
  "B",
@@ -98,8 +104,8 @@ select = [
98
104
  "W",
99
105
  ]
100
106
  ignore = [
101
- "E501", # line too long; if Black does its job, not worried about the rare long line
107
+ "E501", # line too long; if formatter does its job, not worried about the rare long line
102
108
  ]
103
109
 
104
- [tool.ruff.pydocstyle]
110
+ [tool.ruff.lint.pydocstyle]
105
111
  convention = "pep257"
@@ -0,0 +1,236 @@
1
+ """
2
+ This script automates the release process for a Python package.
3
+
4
+ It will:
5
+ - Add a git tag for the given version.
6
+ - Remove the previous dist folder.
7
+ - Create a build.
8
+ - Ask the user to verify the build.
9
+ - Upload the build to PyPI.
10
+ - Push all git tags to the remote.
11
+ - Create a draft release on GitHub using the version notes in CHANGELOG.md.
12
+
13
+ Prerequisites:
14
+ - This must be run from the root of the repository.
15
+ - The repo must have a clean git working tree.
16
+ - The user must have the GITHUB_TOKEN environment variable set to a GitHub personal access token with repository "Contents" read and write permission.
17
+ - The user will need credentials for the PyPI repository, which the user will be prompted for during the upload step. The user will need to paste the token manually from a password manager or similar.
18
+ - The CHANGELOG.md file must already contain an entry for the version being released.
19
+ - Install requirements with: pip install --upgrade --editable '.[release]'
20
+
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import contextlib
26
+ import os
27
+ import re
28
+ import subprocess
29
+ import sys
30
+ from collections.abc import Iterator
31
+ from pathlib import Path
32
+
33
+ import requests
34
+
35
+
36
+ @contextlib.contextmanager
37
+ def add_git_tag_for_version(version: str) -> Iterator[None]:
38
+ """Add a git tag for the given version."""
39
+ subprocess.run(["git", "tag", "-a", version, "-m", version], check=True)
40
+ print(f"Version {version} tag added successfully.")
41
+ try:
42
+ yield
43
+ except:
44
+ subprocess.run(["git", "tag", "-d", version])
45
+ raise
46
+
47
+
48
+ def remove_previous_dist() -> None:
49
+ """Check for dist folder, and if it exists, remove it."""
50
+ subprocess.run(["rm", "-rf", Path("dist")], check=True)
51
+ print("Previous dist folder removed successfully.")
52
+
53
+
54
+ def create_build() -> None:
55
+ """Create a build."""
56
+ subprocess.run(["python", "-m", "build"], check=True)
57
+ print("Build created successfully.")
58
+
59
+
60
+ def verify_build(is_test: str) -> None:
61
+ """Verify the build.
62
+
63
+ Print the archives in dist/ and ask the user to manually inspect and
64
+ confirm they contain the expected files, e.g. source files and test files.
65
+ """
66
+ build_files = os.listdir("dist")
67
+ if len(build_files) != 2:
68
+ print(
69
+ "WARNING: dist folder contains incorrect number of files.", file=sys.stderr
70
+ )
71
+ print("Contents of dist folder:")
72
+ subprocess.run(["ls", "-l", Path("dist")], check=True)
73
+ print("Contents of tar files in dist folder:")
74
+ for build_file in build_files:
75
+ subprocess.run(["tar", "tvf", Path("dist") / build_file], check=True)
76
+ confirmation = input("Does the build look correct? (y/n): ")
77
+ if confirmation == "y":
78
+ print("Build verified successfully.")
79
+ else:
80
+ raise Exception("Could not verify. Build was not uploaded.")
81
+
82
+
83
+ def generate_github_release_notes_body(token: str, version: str) -> str:
84
+ """Generate and grab release notes URL from Github.
85
+
86
+ Delete their first paragraph, because we track its contents in a tighter
87
+ form in CHANGELOG.md. See `get_changelog_release_notes`.
88
+ """
89
+ response = requests.post(
90
+ "https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes",
91
+ headers={
92
+ "Accept": "application/vnd.github+json",
93
+ "Authorization": f"Bearer {token}",
94
+ "X-GitHub-Api-Version": "2022-11-28",
95
+ },
96
+ json={"tag_name": version},
97
+ )
98
+
99
+ try:
100
+ response.raise_for_status()
101
+ except requests.exceptions.HTTPError as err:
102
+ print(
103
+ f"WARNING: Failed to generate release notes from Github: {err}",
104
+ file=sys.stderr,
105
+ )
106
+ return ""
107
+
108
+ body = str(response.json()["body"])
109
+ paragraphs = body.split("\n\n")
110
+ return "\n\n".join(paragraphs[1:])
111
+
112
+
113
+ def get_changelog_release_notes(version: str) -> str:
114
+ """Get the changelog release notes.
115
+
116
+ Uses a regex starting on a heading beginning with the version number
117
+ literal, and matching until the next heading. Using regex to match markup
118
+ is brittle. Consider a Markdown-parsing library instead.
119
+ """
120
+ with open("CHANGELOG.md") as file:
121
+ changelog_text = file.read()
122
+ pattern = re.compile(rf"## {re.escape(version)}[^\n]*(.*?)## ", re.DOTALL)
123
+ match = pattern.search(changelog_text)
124
+ if match:
125
+ return str(match.group(1)).strip()
126
+ else:
127
+ return ""
128
+
129
+
130
+ def create_github_release_draft(token: str, version: str) -> None:
131
+ """Create a release on GitHub."""
132
+ github_release_body = generate_github_release_notes_body(token, version)
133
+ changelog_notes = get_changelog_release_notes(version)
134
+ release_body = f"{changelog_notes}\n\n{github_release_body}"
135
+
136
+ response = requests.post(
137
+ "https://api.github.com/repos/john-kurkowski/tldextract/releases",
138
+ headers={
139
+ "Accept": "application/vnd.github+json",
140
+ "Authorization": f"Bearer {token}",
141
+ "X-GitHub-Api-Version": "2022-11-28",
142
+ },
143
+ json={
144
+ "tag_name": version,
145
+ "name": version,
146
+ "body": release_body,
147
+ "draft": True,
148
+ "prerelease": False,
149
+ },
150
+ )
151
+
152
+ try:
153
+ response.raise_for_status()
154
+ except requests.exceptions.HTTPError as err:
155
+ print(
156
+ f"WARNING: Failed to create release on Github: {err}",
157
+ file=sys.stderr,
158
+ )
159
+ return
160
+
161
+ print(f'Release created successfully: {response.json()["html_url"]}')
162
+
163
+ if not changelog_notes:
164
+ print(
165
+ "WARNING: Failed to parse changelog release notes. Manually copy this version's notes from the CHANGELOG.md file to the above URL.",
166
+ file=sys.stderr,
167
+ )
168
+
169
+
170
+ def upload_build_to_pypi(is_test: str) -> None:
171
+ """Upload the build to PyPI."""
172
+ repository: list[str | Path] = (
173
+ [] if is_test == "n" else ["--repository", "testpypi"]
174
+ )
175
+ upload_command = ["twine", "upload", *repository, Path("dist") / "*"]
176
+ subprocess.run(
177
+ upload_command,
178
+ check=True,
179
+ )
180
+
181
+
182
+ def push_git_tags() -> None:
183
+ """Push all git tags to the remote."""
184
+ subprocess.run(["git", "push", "--tags", "origin", "master"], check=True)
185
+
186
+
187
+ def check_for_clean_working_tree() -> None:
188
+ """Check for a clean git working tree."""
189
+ git_status = subprocess.run(
190
+ ["git", "status", "--porcelain"], capture_output=True, text=True
191
+ )
192
+ if git_status.stdout:
193
+ print(
194
+ "Git working tree is not clean. Please commit or stash changes.",
195
+ file=sys.stderr,
196
+ )
197
+ sys.exit(1)
198
+
199
+
200
+ def get_env_github_token() -> str:
201
+ """Check for the GITHUB_TOKEN environment variable."""
202
+ github_token = os.environ.get("GITHUB_TOKEN")
203
+ if not github_token:
204
+ print("GITHUB_TOKEN environment variable not set.", file=sys.stderr)
205
+ sys.exit(1)
206
+ return github_token
207
+
208
+
209
+ def get_is_test_response() -> str:
210
+ """Ask the user if this is a test release."""
211
+ while True:
212
+ is_test = input("Is this a test release? (y/n): ")
213
+ if is_test in ["y", "n"]:
214
+ return is_test
215
+ else:
216
+ print("Invalid input. Please enter 'y' or 'n.'")
217
+
218
+
219
+ def main() -> None:
220
+ """Run the main program."""
221
+ check_for_clean_working_tree()
222
+ github_token = get_env_github_token()
223
+ is_test = get_is_test_response()
224
+ version_number = input("Enter the version number: ")
225
+
226
+ with add_git_tag_for_version(version_number):
227
+ remove_previous_dist()
228
+ create_build()
229
+ verify_build(is_test)
230
+ upload_build_to_pypi(is_test)
231
+ push_git_tags()
232
+ create_github_release_draft(github_token, version_number)
233
+
234
+
235
+ if __name__ == "__main__":
236
+ main()