dmcaed 2026.4.18__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,3 @@
1
+ export VIRTUAL_ENV=.venv
2
+ layout python python3.11
3
+ dotenv
@@ -0,0 +1,5 @@
1
+ DMCA.db
2
+ exports/
3
+ __pycache__/
4
+ dist/
5
+ .env
@@ -0,0 +1,6 @@
1
+ {
2
+ "cSpell.words": [
3
+ "dmca",
4
+ "dmcaed"
5
+ ]
6
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Canarduck
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,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: dmcaed
3
+ Version: 2026.4.18
4
+ Summary: Check if a software URL is found in takedown notices.
5
+ Project-URL: Homepage, https://codeberg.org/canarduck/dmcaed
6
+ Project-URL: Issues, https://codeberg.org/canarduck/dmcaed/issues
7
+ Author-email: Renaud Canarduck <renaud@canarduck.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.11
14
+ Provides-Extra: dev
15
+ Requires-Dist: black; extra == 'dev'
16
+ Requires-Dist: mypy; extra == 'dev'
17
+ Provides-Extra: tests
18
+ Requires-Dist: coverage; extra == 'tests'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # DMCAed
22
+
23
+ A command-line tool to analyze GitHub DMCA takedown notices. It downloads, parses, and
24
+ indexes takedown data from the [GitHub DMCA repository](https://github.com/github/dmca)
25
+ to allow querying **which repository URL can be found** in takedown requests, please
26
+ note that this only means the repository URL _might_ have been subject to takedown
27
+ requests.
28
+
29
+ ## Installation
30
+
31
+ DMCAed requires at least python 3.11.
32
+
33
+ ```bash
34
+ pip install dmcaed
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ ```bash
40
+ dmcaed [--verbose] [command]
41
+ ```
42
+
43
+ ### Commands
44
+
45
+ - `download`: Download the latest DMCA notices from GitHub
46
+ - `analyze`: Parse downloaded notices and extract takedown data
47
+ - `db`: Rebuild the complete DMCA database (download + analyze)
48
+ - `stats`: Show statistics about the database content
49
+ - `notices <url>`: List takedown notices for a specific repository URL
50
+
51
+ ### Options
52
+
53
+ - `--verbose`: Enable debug logging
54
+ - `-h`, `--help`: Show help message
55
+
56
+ ## Library Usage
57
+
58
+ ```python
59
+ from dmcaed import DMCAed
60
+
61
+ dmca = DMCAed()
62
+ notices = dmca.notices("https://github.com/user/repo")
63
+
64
+ if notices:
65
+ print(f"https://github.com/user/repo _might_ be subject to a takedown: {notices}")
66
+ ```
67
+
68
+ ## How it works
69
+
70
+ - Download the latest DMCA notices from GitHub
71
+ - Extract repository URLs (path to files, issues, releases, etc. are truncated)
72
+ mentioned in takedown notices
73
+ - Build a local SQLite database mapping repository URLs to takedown notices
74
+
75
+ ## Build and Release
76
+
77
+ ### Prerequisites
78
+
79
+ The project uses `hatch` build system and dynamic CalVer versioning based on the
80
+ current date.
81
+
82
+ ```bash
83
+ pip install hatch hatch-calver
84
+ ```
85
+
86
+ Git clone and install DMCAed.
87
+
88
+ ```bash
89
+ git clone ssh://git@codeberg.org/canarduck/dmcaed.git
90
+ pip install -e .
91
+ ```
92
+
93
+ ### Update the package and publish it on PyPI
94
+
95
+ You first need to fetch DMCAs and create/update the database.
96
+
97
+ ```bash
98
+ dmcaed db
99
+ ```
100
+
101
+ Then use hatch to update version number and build the package.
102
+
103
+ ```bash
104
+ hatch version release
105
+ # use `hatch version release,fix` in case you need to fix an already released package
106
+ ```
107
+
108
+ Build the package and publish it on PyPI.
109
+
110
+ ```bash
111
+ hatch build
112
+ hatch publish --auth PYPI_AUTH_TOKEN
113
+ ```
@@ -0,0 +1,93 @@
1
+ # DMCAed
2
+
3
+ A command-line tool to analyze GitHub DMCA takedown notices. It downloads, parses, and
4
+ indexes takedown data from the [GitHub DMCA repository](https://github.com/github/dmca)
5
+ to allow querying **which repository URL can be found** in takedown requests, please
6
+ note that this only means the repository URL _might_ have been subject to takedown
7
+ requests.
8
+
9
+ ## Installation
10
+
11
+ DMCAed requires at least python 3.11.
12
+
13
+ ```bash
14
+ pip install dmcaed
15
+ ```
16
+
17
+ ## CLI
18
+
19
+ ```bash
20
+ dmcaed [--verbose] [command]
21
+ ```
22
+
23
+ ### Commands
24
+
25
+ - `download`: Download the latest DMCA notices from GitHub
26
+ - `analyze`: Parse downloaded notices and extract takedown data
27
+ - `db`: Rebuild the complete DMCA database (download + analyze)
28
+ - `stats`: Show statistics about the database content
29
+ - `notices <url>`: List takedown notices for a specific repository URL
30
+
31
+ ### Options
32
+
33
+ - `--verbose`: Enable debug logging
34
+ - `-h`, `--help`: Show help message
35
+
36
+ ## Library Usage
37
+
38
+ ```python
39
+ from dmcaed import DMCAed
40
+
41
+ dmca = DMCAed()
42
+ notices = dmca.notices("https://github.com/user/repo")
43
+
44
+ if notices:
45
+ print(f"https://github.com/user/repo _might_ be subject to a takedown: {notices}")
46
+ ```
47
+
48
+ ## How it works
49
+
50
+ - Download the latest DMCA notices from GitHub
51
+ - Extract repository URLs (path to files, issues, releases, etc. are truncated)
52
+ mentioned in takedown notices
53
+ - Build a local SQLite database mapping repository URLs to takedown notices
54
+
55
+ ## Build and Release
56
+
57
+ ### Prerequisites
58
+
59
+ The project uses `hatch` build system and dynamic CalVer versioning based on the
60
+ current date.
61
+
62
+ ```bash
63
+ pip install hatch hatch-calver
64
+ ```
65
+
66
+ Git clone and install DMCAed.
67
+
68
+ ```bash
69
+ git clone ssh://git@codeberg.org/canarduck/dmcaed.git
70
+ pip install -e .
71
+ ```
72
+
73
+ ### Update the package and publish it on PyPI
74
+
75
+ You first need to fetch DMCAs and create/update the database.
76
+
77
+ ```bash
78
+ dmcaed db
79
+ ```
80
+
81
+ Then use hatch to update version number and build the package.
82
+
83
+ ```bash
84
+ hatch version release
85
+ # use `hatch version release,fix` in case you need to fix an already released package
86
+ ```
87
+
88
+ Build the package and publish it on PyPI.
89
+
90
+ ```bash
91
+ hatch build
92
+ hatch publish --auth PYPI_AUTH_TOKEN
93
+ ```
@@ -0,0 +1,205 @@
1
+ """DMCAed.
2
+
3
+ Check if a software URL is found in takedown notices.
4
+ """
5
+
6
+ import argparse
7
+ import logging
8
+ import re
9
+ import shutil
10
+ import sqlite3
11
+ import sys
12
+ import urllib.request
13
+ import zipfile
14
+ from pathlib import Path
15
+ from importlib.resources import files
16
+
17
+ __version__ = "2026.4.18"
18
+
19
+ logger = logging.getLogger("dmcaed")
20
+
21
+ LOCAL_DB = Path("DMCA.db")
22
+ EXPORTS = Path("exports")
23
+
24
+ GH_DMCA_URL = "https://github.com/github/dmca"
25
+ """GH DMCA repo URL."""
26
+ GH_DMCA_DOWNLOAD_URL = f"{GH_DMCA_URL}/archive/refs/heads/master.zip"
27
+ """The URL to Github's zip export of their DMCA repo."""
28
+
29
+
30
+ class DMCAed:
31
+ def __init__(self, db: Path | None = None):
32
+ """Init the db cursor."""
33
+ if not db:
34
+ ref = files(__name__) / "DMCA.db"
35
+ db = Path(str(ref))
36
+ self.cursor = sqlite3.connect(str(db)).cursor()
37
+
38
+ def notices(self, origin_url: str) -> set[str]:
39
+ """Get the takedown notices associated to `origin_url`.
40
+
41
+ The check ignore the URL case.
42
+
43
+ Args:
44
+ origin_url: a software URL.
45
+
46
+ Returns:
47
+ A set of takedown URLs, the set is empty if nothing matches.
48
+ """
49
+ self.cursor.execute(
50
+ "SELECT notice_url FROM takedowns WHERE origin_url = ? COLLATE NOCASE",
51
+ [origin_url],
52
+ )
53
+ return {r[0] for r in self.cursor.fetchall()}
54
+
55
+ def stats(self) -> dict:
56
+ """Some stats on the db.
57
+
58
+ Returns:
59
+ A dict of stats
60
+ """
61
+ self.cursor.execute(
62
+ "SELECT COUNT(DISTINCT origin_url), COUNT(DISTINCT notice_url) FROM takedowns"
63
+ )
64
+ origins, notices = self.cursor.fetchone()
65
+ return {"origins": origins, "notices": notices}
66
+
67
+
68
+ def download_notices() -> None:
69
+ logger.info(f"Cleanup {EXPORTS}")
70
+ shutil.rmtree(EXPORTS, ignore_errors=True)
71
+ EXPORTS.mkdir()
72
+ gh_zip_path = EXPORTS / "gh.zip"
73
+
74
+ logger.info(f"Download {GH_DMCA_DOWNLOAD_URL}")
75
+ urllib.request.urlretrieve(GH_DMCA_DOWNLOAD_URL, gh_zip_path)
76
+
77
+ logger.info(f"Extract to {EXPORTS}")
78
+ with zipfile.ZipFile(gh_zip_path, "r") as z:
79
+ z.extractall(EXPORTS)
80
+
81
+
82
+ def analyze_notices() -> None:
83
+ logger.info("Create an in-memory database")
84
+ db = sqlite3.connect(":memory:")
85
+ db.execute(
86
+ "CREATE TABLE takedowns (origin_url TEXT NOT NULL, notice_url TEXT NOT NULL)"
87
+ )
88
+ db.execute("CREATE INDEX origin_url_idx ON takedowns(origin_url COLLATE NOCASE)")
89
+
90
+ logger.info("Extracting repo URLS")
91
+
92
+ gh_dmca_path = EXPORTS / "dmca-master"
93
+
94
+ notice_paths = list(gh_dmca_path.glob("[0-9][0-9][0-9][0-9]/**/*.md")) + list(
95
+ gh_dmca_path.glob("[0-9][0-9][0-9][0-9]/**/*.markdown")
96
+ )
97
+
98
+ logger.info(f"{len(notice_paths)} notices to process")
99
+
100
+ cursor = db.cursor()
101
+
102
+ def gh_path_to_url(path: Path) -> str:
103
+ return str(path).replace("exports/dmca-master", f"{GH_DMCA_URL}/blob/master")
104
+
105
+ origin_pattern = re.compile(r"https?://github\.com/[\w\.\-]+/[\w\.\-\_]+")
106
+ for notice_path in notice_paths:
107
+ logger.debug("Processing %s", notice_path)
108
+ for origin_url in origin_pattern.findall(notice_path.read_text()):
109
+ logger.debug("- Found %s", origin_url)
110
+ cursor.execute(
111
+ "INSERT INTO takedowns(origin_url, notice_url) VALUES(?,?)",
112
+ [origin_url, gh_path_to_url(notice_path)],
113
+ )
114
+ logger.debug("Commit DB")
115
+ db.commit()
116
+ logger.debug(f"Cleanup previous {LOCAL_DB}")
117
+ LOCAL_DB.unlink(missing_ok=True)
118
+ db.execute(f"VACUUM main INTO '{LOCAL_DB}'")
119
+ logger.info(f"Wrote {LOCAL_DB}")
120
+
121
+
122
+ def build_db() -> None:
123
+ """Create a new DMCA db.
124
+
125
+ This method:
126
+ 1. recreate `LOCAL_DB`
127
+ 2. fetches the latest version of GH DMCA repo to `exports/`
128
+ 3. extract repo URLs from the takedown notices
129
+ 4. store a (repo URL, Takedown URL) pair in the DB
130
+ """
131
+ logger.info(f"Rebuilding {LOCAL_DB}")
132
+ download_notices()
133
+ analyze_notices()
134
+
135
+ dmcaed = DMCAed(LOCAL_DB)
136
+
137
+ logger.info(f"Done! {dmcaed.stats()}")
138
+
139
+
140
+ def main():
141
+ parser = argparse.ArgumentParser(
142
+ description="Check if a software URL is found in takedown notices"
143
+ )
144
+ parser.add_argument("--verbose", action="store_true", help="Enable debug logging")
145
+ parser.add_argument(
146
+ "--local",
147
+ type=bool,
148
+ help="Use the local database file. Uses the packaged one by default.",
149
+ )
150
+
151
+ subparsers = parser.add_subparsers(dest="command", required=True)
152
+ subparsers.add_parser(
153
+ "download", help="Download the latest DMCA notices from GitHub"
154
+ )
155
+ subparsers.add_parser(
156
+ "analyze", help="Parse downloaded notices and extract takedown data"
157
+ )
158
+ subparsers.add_parser(
159
+ "db", help="Rebuild the complete DMCA database (download + analyze)"
160
+ )
161
+ subparsers.add_parser("stats", help="Show statistics about the database content")
162
+ parser_notices = subparsers.add_parser(
163
+ "notices", help="List takedown notices for a URL"
164
+ )
165
+ parser_notices.add_argument("url", help="The origin URL to check")
166
+
167
+ if len(sys.argv) == 1:
168
+ print(r""" ____ __ ____________ __
169
+ / __ \/ |/ / ____/ | ___ ____/ /
170
+ / / / / /|_/ / / / /| |/ _ \/ __ /
171
+ / /_/ / / / / /___/ ___ / __/ /_/ /
172
+ /_____/_/ /_/\____/_/ |_\___/\__,_/
173
+ """)
174
+ parser.print_help()
175
+ sys.exit(0)
176
+
177
+ args = parser.parse_args()
178
+
179
+ logging.basicConfig(
180
+ level=logging.DEBUG if args.verbose else logging.INFO,
181
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
182
+ )
183
+
184
+ if args.command == "download":
185
+ download_notices()
186
+ elif args.command == "analyze":
187
+ analyze_notices()
188
+ elif args.command == "db":
189
+ build_db()
190
+ elif args.command == "stats":
191
+ db = LOCAL_DB if args.local else None
192
+ dmcaed = DMCAed(db)
193
+ print(dmcaed.stats())
194
+ elif args.command == "notices":
195
+ db = LOCAL_DB if args.local else None
196
+ dmcaed = DMCAed(db)
197
+ notices = dmcaed.notices(args.url)
198
+ if notices:
199
+ print(*notices, sep="\n")
200
+ else:
201
+ print("No takedown notices found for the given URL.")
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "dmcaed"
3
+ authors = [
4
+ { name="Renaud Canarduck", email="renaud@canarduck.com" },
5
+ ]
6
+ description = "Check if a software URL is found in takedown notices."
7
+ readme = "README.md"
8
+ requires-python = ">=3.11"
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3",
11
+ "Operating System :: OS Independent",
12
+ "Development Status :: 3 - Alpha"
13
+ ]
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ dynamic = ["version"]
17
+
18
+ [project.urls]
19
+ Homepage = "https://codeberg.org/canarduck/dmcaed"
20
+ Issues = "https://codeberg.org/canarduck/dmcaed/issues"
21
+
22
+ [project.optional-dependencies]
23
+ tests = ["coverage"]
24
+ dev = [
25
+ "black",
26
+ "mypy",
27
+ ]
28
+
29
+ [project.scripts]
30
+ dmcaed = "dmcaed:main"
31
+
32
+ [project.data]
33
+ paths = ["DMCA.db"]
34
+
35
+ [build-system]
36
+ requires = ["hatchling >= 1.26", "hatch-calver"]
37
+ build-backend = "hatchling.build"
38
+
39
+ [tool.hatch.version]
40
+ path = "dmcaed.py"
41
+ scheme = "calver"
42
+ calver-scheme = "YYYY.MM.DD"
43
+
44
+ [tool.coverage.report]
45
+ fail_under = 90
46
+
47
+ [tool.coverage.run]
48
+ branch = true
49
+ source = ["."]