docdrop 0.1.0__py3-none-any.whl

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.
docdrop/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """The docdrop CLI."""
2
+
3
+ __version__ = "0.1.0"
docdrop/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
docdrop/cli.py ADDED
@@ -0,0 +1,288 @@
1
+ """docdrop — publish HTML and Markdown documents by unguessable link.
2
+
3
+ Every command accepts a global [--server NAME] prefix to target a named
4
+ server profile from your config (see 'docdrop servers').
5
+
6
+ docdrop [--server NAME] publish <file> [--slug SLUG] [--title TITLE]
7
+ [--alias NAME | --clear-alias]
8
+ docdrop [--server NAME] list
9
+ docdrop [--server NAME] open <slug> [--token]
10
+ docdrop [--server NAME] alias <slug> [name] [--clear]
11
+ docdrop [--server NAME] grant <slug> [--label NAME] [--expires WHEN]
12
+ docdrop [--server NAME] revoke <slug> <handle>
13
+ docdrop [--server NAME] grants <slug>
14
+ docdrop [--server NAME] history <slug>
15
+ docdrop [--server NAME] diff <slug> [A B]
16
+ docdrop [--server NAME] rollback <slug> N
17
+ docdrop servers
18
+ """
19
+
20
+ import argparse
21
+ import sys
22
+ import webbrowser
23
+ from pathlib import Path
24
+
25
+ from . import config as config_module
26
+ from .client import Client, DocDropError
27
+ from .config import ConfigError
28
+ from .documents import UnsupportedFile, content_type_of, slug_of, title_of
29
+
30
+
31
+ def publish(client: Client, args: argparse.Namespace) -> int:
32
+ path: Path = args.file
33
+ if not path.is_file():
34
+ raise DocDropError(f"No such file: {path}")
35
+
36
+ content_type = content_type_of(path)
37
+ content = path.read_text()
38
+ slug = args.slug or slug_of(path)
39
+ title = args.title or title_of(content, content_type) or slug
40
+
41
+ result = client.publish(
42
+ slug,
43
+ title,
44
+ content,
45
+ content_type,
46
+ alias=args.alias,
47
+ clear_alias=args.clear_alias,
48
+ )
49
+ print(f"{result['action']}: {result['title']} ({result['content_type']})")
50
+ if result.get("alias_url"):
51
+ print(result["alias_url"])
52
+ print(result["url"])
53
+ return 0
54
+
55
+
56
+ def list_documents(client: Client, args: argparse.Namespace) -> int:
57
+ documents = client.documents()
58
+ if not documents:
59
+ print("No documents published yet.")
60
+ return 0
61
+ for doc in documents:
62
+ print(f"{doc['slug']:24} {doc['updated'][:16]:17} {doc['url']}")
63
+ print(f"{'':24} {doc['title']}")
64
+ if doc.get("alias_url"):
65
+ print(f"{'':24} {doc['alias_url']}")
66
+ return 0
67
+
68
+
69
+ def open_document(client: Client, args: argparse.Namespace) -> int:
70
+ for doc in client.documents():
71
+ if doc["slug"] == args.slug:
72
+ url = (
73
+ doc["url"]
74
+ if args.token or not doc.get("alias_url")
75
+ else doc["alias_url"]
76
+ )
77
+ print(url)
78
+ webbrowser.open(url)
79
+ return 0
80
+ raise DocDropError(f"You have no document with the slug {args.slug!r}.")
81
+
82
+
83
+ def alias_command(client: Client, args: argparse.Namespace) -> int:
84
+ result = client.alias(args.slug, args.name, args.clear)
85
+ if args.name:
86
+ print(result["alias_url"])
87
+ print(result["url"])
88
+ elif args.clear:
89
+ print(f"{args.slug}: alias cleared")
90
+ elif result.get("alias"):
91
+ print(result["alias_url"])
92
+ else:
93
+ print(f"no alias set for {args.slug}")
94
+ return 0
95
+
96
+
97
+ def grant_command(client: Client, args: argparse.Namespace) -> int:
98
+ result = client.grant(args.slug, args.label, args.expires)
99
+ print(result["url"])
100
+ print(f"handle: {result['handle']} label: {result['label']}")
101
+ return 0
102
+
103
+
104
+ def revoke_command(client: Client, args: argparse.Namespace) -> int:
105
+ client.revoke(args.slug, args.handle)
106
+ print(f"{args.slug}: revoked {args.handle}")
107
+ return 0
108
+
109
+
110
+ def grants_command(client: Client, args: argparse.Namespace) -> int:
111
+ grants = client.grants(args.slug)
112
+ if not grants:
113
+ print(f"No grants for {args.slug}.")
114
+ return 0
115
+ for grant in grants:
116
+ last_viewed = grant["last_viewed"][:16] if grant.get("last_viewed") else "never"
117
+ expires = grant["expires"][:16] if grant.get("expires") else "never"
118
+ marker = " (revoked)" if grant.get("revoked") else ""
119
+ print(
120
+ f"{grant['handle']:10} {grant.get('label', ''):14} "
121
+ f"views:{grant['view_count']:<4} last:{last_viewed:17}{marker}"
122
+ )
123
+ print(f"{'':10} created:{grant['created'][:16]:17} expires:{expires}")
124
+ return 0
125
+
126
+
127
+ def history_command(client: Client, args: argparse.Namespace) -> int:
128
+ revisions = client.history(args.slug)
129
+ if not revisions:
130
+ print(f"No revisions for {args.slug}.")
131
+ return 0
132
+ for r in revisions:
133
+ marker = " (head)" if r.get("head") else ""
134
+ print(
135
+ f"{r['number']:<4} {r['published'][:16]:17} "
136
+ f"{r['content_type']:14}{marker}"
137
+ )
138
+ return 0
139
+
140
+
141
+ def diff_command(client: Client, args: argparse.Namespace) -> int:
142
+ if args.revisions and len(args.revisions) != 2:
143
+ raise DocDropError("diff takes zero or two revision numbers.")
144
+ a, b = args.revisions if args.revisions else (None, None)
145
+ result = client.diff(args.slug, a, b)
146
+ print(result["diff"], end="")
147
+ return 0
148
+
149
+
150
+ def rollback_command(client: Client, args: argparse.Namespace) -> int:
151
+ result = client.rollback(args.slug, args.number)
152
+ print(f"{args.slug}: rolled back to {args.number}, new head is {result['number']}")
153
+ print(result["url"])
154
+ return 0
155
+
156
+
157
+ def servers_command(args: argparse.Namespace) -> int:
158
+ names, active = config_module.list_servers(server=args.server)
159
+ for name in names:
160
+ marker = "* " if name == active else " "
161
+ print(f"{marker}{name}")
162
+ return 0
163
+
164
+
165
+ def build_parser() -> argparse.ArgumentParser:
166
+ parser = argparse.ArgumentParser(
167
+ prog="docdrop",
168
+ description="Publish HTML and Markdown documents by unguessable link.",
169
+ )
170
+ parser.add_argument("--server", help="named profile (see 'docdrop servers')")
171
+ commands = parser.add_subparsers(dest="command", required=True)
172
+
173
+ publish_cmd = commands.add_parser(
174
+ "publish", help="publish an HTML or Markdown file"
175
+ )
176
+ publish_cmd.add_argument("file", type=Path, help="the file to publish")
177
+ publish_cmd.add_argument(
178
+ "--slug", help="URL-stable document key (default: the file's name)"
179
+ )
180
+ publish_cmd.add_argument(
181
+ "--title", help="document title (default: the <title>, or the Markdown H1)"
182
+ )
183
+ alias_group = publish_cmd.add_mutually_exclusive_group()
184
+ alias_group.add_argument("--alias", help="set or edit the document's alias")
185
+ alias_group.add_argument(
186
+ "--clear-alias",
187
+ action="store_true",
188
+ help="remove the document's alias",
189
+ )
190
+ publish_cmd.set_defaults(run=publish)
191
+
192
+ list_cmd = commands.add_parser("list", help="list the documents you've published")
193
+ list_cmd.set_defaults(run=list_documents)
194
+
195
+ servers_cmd = commands.add_parser(
196
+ "servers", help="list your configured server profiles"
197
+ )
198
+ servers_cmd.set_defaults(run=servers_command)
199
+
200
+ open_cmd = commands.add_parser("open", help="open a document in your browser")
201
+ open_cmd.add_argument("slug", help="the document's slug")
202
+ open_cmd.add_argument(
203
+ "--token",
204
+ action="store_true",
205
+ help="open the token URL even when an alias exists",
206
+ )
207
+ open_cmd.set_defaults(run=open_document)
208
+
209
+ alias_cmd = commands.add_parser(
210
+ "alias", help="add, edit, clear, or show a document's alias"
211
+ )
212
+ alias_cmd.add_argument("slug", help="the document's slug")
213
+ alias_cmd.add_argument(
214
+ "name", nargs="?", help="the alias to set (omit to print the current alias)"
215
+ )
216
+ alias_cmd.add_argument("--clear", action="store_true", help="remove the alias")
217
+ alias_cmd.set_defaults(run=alias_command)
218
+
219
+ grant_cmd = commands.add_parser(
220
+ "grant", help="mint a new share link for a document"
221
+ )
222
+ grant_cmd.add_argument("slug", help="the document's slug")
223
+ grant_cmd.add_argument(
224
+ "--label", help="a name for this link (default: auto-assigned)"
225
+ )
226
+ grant_cmd.add_argument(
227
+ "--expires",
228
+ help=(
229
+ "when this link expires: a duration (7d, 24h) or an ISO date "
230
+ "(2026-08-01); omit for a link that never expires"
231
+ ),
232
+ )
233
+ grant_cmd.set_defaults(run=grant_command)
234
+
235
+ revoke_cmd = commands.add_parser("revoke", help="revoke a document's share link")
236
+ revoke_cmd.add_argument("slug", help="the document's slug")
237
+ revoke_cmd.add_argument(
238
+ "handle", help="the link's short handle, from `docdrop grants`"
239
+ )
240
+ revoke_cmd.set_defaults(run=revoke_command)
241
+
242
+ grants_cmd = commands.add_parser("grants", help="list a document's share links")
243
+ grants_cmd.add_argument("slug", help="the document's slug")
244
+ grants_cmd.set_defaults(run=grants_command)
245
+
246
+ history_cmd = commands.add_parser("history", help="list a document's revisions")
247
+ history_cmd.add_argument("slug", help="the document's slug")
248
+ history_cmd.set_defaults(run=history_command)
249
+
250
+ diff_cmd = commands.add_parser(
251
+ "diff", help="show what changed between two revisions"
252
+ )
253
+ diff_cmd.add_argument("slug", help="the document's slug")
254
+ diff_cmd.add_argument(
255
+ "revisions",
256
+ nargs="*",
257
+ type=int,
258
+ metavar="N",
259
+ help="two revision numbers to diff (default: previous vs. head)",
260
+ )
261
+ diff_cmd.set_defaults(run=diff_command)
262
+
263
+ rollback_cmd = commands.add_parser(
264
+ "rollback", help="make an old revision the new head"
265
+ )
266
+ rollback_cmd.add_argument("slug", help="the document's slug")
267
+ rollback_cmd.add_argument(
268
+ "number", type=int, help="the revision number to roll back to"
269
+ )
270
+ rollback_cmd.set_defaults(run=rollback_command)
271
+
272
+ return parser
273
+
274
+
275
+ def main(argv: list[str] | None = None) -> int:
276
+ args = build_parser().parse_args(argv)
277
+ try:
278
+ if args.command == "servers":
279
+ return args.run(args)
280
+ client = Client(config_module.load(server=args.server))
281
+ return args.run(client, args)
282
+ except (ConfigError, DocDropError, UnsupportedFile) as exc:
283
+ print(exc, file=sys.stderr)
284
+ return 1
285
+
286
+
287
+ if __name__ == "__main__":
288
+ sys.exit(main())
docdrop/client.py ADDED
@@ -0,0 +1,104 @@
1
+ """The HTTP client for a DocDrop app."""
2
+
3
+ import httpx
4
+
5
+ from .config import Config
6
+
7
+ TIMEOUT = httpx.Timeout(30.0, connect=10.0)
8
+
9
+
10
+ class DocDropError(Exception):
11
+ """Something the user needs to read: a refusal, or an unreachable app."""
12
+
13
+
14
+ class Client:
15
+ def __init__(self, config: Config):
16
+ self._config = config
17
+
18
+ def publish(
19
+ self,
20
+ slug: str,
21
+ title: str,
22
+ content: str,
23
+ content_type: str,
24
+ alias: str | None = None,
25
+ clear_alias: bool = False,
26
+ ) -> dict:
27
+ body = {
28
+ "slug": slug,
29
+ "title": title,
30
+ "content": content,
31
+ "content_type": content_type,
32
+ }
33
+ if clear_alias:
34
+ body["clear_alias"] = True
35
+ elif alias is not None:
36
+ body["alias"] = alias
37
+ return self._call("POST", "/api/publish", json=body)
38
+
39
+ def alias(self, slug: str, name: str | None = None, clear: bool = False) -> dict:
40
+ body = {"slug": slug}
41
+ if clear:
42
+ body["clear"] = True
43
+ elif name is not None:
44
+ body["name"] = name
45
+ return self._call("POST", "/api/alias", json=body)
46
+
47
+ def documents(self) -> list[dict]:
48
+ return self._call("GET", "/api/documents")["documents"]
49
+
50
+ def grant(
51
+ self, slug: str, label: str | None = None, expires: str | None = None
52
+ ) -> dict:
53
+ body = {"slug": slug}
54
+ if label is not None:
55
+ body["label"] = label
56
+ if expires is not None:
57
+ body["expires"] = expires
58
+ return self._call("POST", "/api/grant", json=body)
59
+
60
+ def revoke(self, slug: str, handle: str) -> dict:
61
+ return self._call(
62
+ "POST", "/api/revoke", json={"slug": slug, "handle": handle}
63
+ )
64
+
65
+ def grants(self, slug: str) -> list[dict]:
66
+ return self._call("GET", "/api/grants", params={"slug": slug})["grants"]
67
+
68
+ def history(self, slug: str) -> list[dict]:
69
+ return self._call("GET", "/api/history", params={"slug": slug})["revisions"]
70
+
71
+ def diff(self, slug: str, a: int | None = None, b: int | None = None) -> dict:
72
+ params = {"slug": slug}
73
+ if a is not None and b is not None:
74
+ params["a"] = a
75
+ params["b"] = b
76
+ return self._call("GET", "/api/diff", params=params)
77
+
78
+ def rollback(self, slug: str, number: int) -> dict:
79
+ return self._call(
80
+ "POST", "/api/rollback", json={"slug": slug, "number": number}
81
+ )
82
+
83
+ def _call(self, method: str, path: str, **kwargs) -> dict:
84
+ url = f"{self._config.url}{path}"
85
+ headers = {"Authorization": f"Bearer {self._config.api_key}"}
86
+ try:
87
+ response = httpx.request(
88
+ method, url, headers=headers, timeout=TIMEOUT, **kwargs
89
+ )
90
+ except httpx.RequestError as exc:
91
+ raise DocDropError(f"Could not reach {self._config.url}: {exc}") from None
92
+
93
+ if response.is_success:
94
+ return response.json()
95
+
96
+ # The API reports refusals as {"error": "..."}; anything else (a proxy
97
+ # error page, say) has no message worth relaying, so show the status.
98
+ try:
99
+ message = response.json().get("error")
100
+ except ValueError:
101
+ message = None
102
+ raise DocDropError(
103
+ message or f"{self._config.url} returned HTTP {response.status_code}."
104
+ )
docdrop/config.py ADDED
@@ -0,0 +1,129 @@
1
+ """Where the API key and server URL live.
2
+
3
+ Config is read from $DOCDROP_CONFIG, else $XDG_CONFIG_HOME/docdrop/config.toml,
4
+ else ~/.config/docdrop/config.toml.
5
+
6
+ With named `[servers.<name>]` profiles, pick one with `--server` (or omit it
7
+ and let the sole profile auto-default, or the one marked `default = true`
8
+ when there are several):
9
+
10
+ [servers.production]
11
+ url = "https://docdrop.anvil.app"
12
+ api_key = "..."
13
+
14
+ [servers.local]
15
+ url = "http://localhost:3030"
16
+ api_key = "..."
17
+ default = true
18
+
19
+ With no `[servers.*]` tables, the flat top-level keys are used instead (and
20
+ DOCDROP_API_KEY / DOCDROP_URL, if set, override them):
21
+
22
+ api_key = "..."
23
+ url = "https://docdrop.anvil.app"
24
+ """
25
+
26
+ import os
27
+ import tomllib
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+
31
+
32
+ class ConfigError(Exception):
33
+ pass
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class Config:
38
+ api_key: str
39
+ url: str
40
+
41
+
42
+ def config_path() -> Path:
43
+ if override := os.environ.get("DOCDROP_CONFIG"):
44
+ return Path(override)
45
+ base = os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config"
46
+ return Path(base) / "docdrop" / "config.toml"
47
+
48
+
49
+ def _read_stored(path: Path) -> dict:
50
+ if not path.exists():
51
+ return {}
52
+ try:
53
+ return tomllib.loads(path.read_text())
54
+ except tomllib.TOMLDecodeError as exc:
55
+ raise ConfigError(f"{path} is not valid TOML: {exc}") from None
56
+
57
+
58
+ def _finalize(api_key: str | None, url: str | None, path: Path) -> Config:
59
+ if not api_key or not url:
60
+ raise ConfigError(
61
+ f"Incomplete config: both api_key and url are required (see {path})."
62
+ )
63
+ return Config(api_key=api_key, url=url.rstrip("/"))
64
+
65
+
66
+ def _servers_or_raise(server: str, servers: dict, path: Path) -> dict:
67
+ """Look up a named profile table, raising ConfigError if not found."""
68
+ if not servers:
69
+ raise ConfigError(
70
+ f"No server profiles defined in {path}. Add a [servers.{server}] "
71
+ "table, or omit --server."
72
+ )
73
+ if server not in servers:
74
+ names = ", ".join(sorted(servers))
75
+ raise ConfigError(f"No profile '{server}' (have: {names}).")
76
+ return servers[server]
77
+
78
+
79
+ def _default_name(servers: dict) -> str | None:
80
+ """Resolve the implicit default profile name, or None if ambiguous."""
81
+ if len(servers) == 1:
82
+ return next(iter(servers))
83
+ marked = [name for name, table in servers.items() if table.get("default") is True]
84
+ return marked[0] if len(marked) == 1 else None
85
+
86
+
87
+ def _default_table(servers: dict, path: Path) -> dict:
88
+ name = _default_name(servers)
89
+ if name is None:
90
+ raise ConfigError(
91
+ f"{len(servers)} server profiles in {path} but the default is "
92
+ f"ambiguous (have: {', '.join(sorted(servers))}). Mark exactly "
93
+ "one default = true, or pass --server."
94
+ )
95
+ return servers[name]
96
+
97
+
98
+ def load(server: str | None = None) -> Config:
99
+ path = config_path()
100
+ stored = _read_stored(path)
101
+ servers = stored.get("servers", {})
102
+
103
+ if server is not None:
104
+ table = _servers_or_raise(server, servers, path)
105
+ return _finalize(table.get("api_key"), table.get("url"), path)
106
+
107
+ if servers:
108
+ table = _default_table(servers, path)
109
+ return _finalize(table.get("api_key"), table.get("url"), path)
110
+
111
+ api_key = os.environ.get("DOCDROP_API_KEY") or stored.get("api_key")
112
+ url = os.environ.get("DOCDROP_URL") or stored.get("url")
113
+ return _finalize(api_key, url, path)
114
+
115
+
116
+ def list_servers(server: str | None = None) -> tuple[list[str], str | None]:
117
+ """Return (profile names, active name) without ever touching an api_key."""
118
+ path = config_path()
119
+ stored = _read_stored(path)
120
+ servers = stored.get("servers", {})
121
+
122
+ if not servers and server is None:
123
+ return ["(default)"], "(default)"
124
+
125
+ if server is not None:
126
+ _servers_or_raise(server, servers, path)
127
+ return sorted(servers), server
128
+
129
+ return sorted(servers), _default_name(servers)
docdrop/documents.py ADDED
@@ -0,0 +1,57 @@
1
+ """Reading a document off disk: what it is, and what it's called."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ HTML = "text/html"
7
+ MARKDOWN = "text/markdown"
8
+
9
+ CONTENT_TYPES = {
10
+ ".html": HTML,
11
+ ".htm": HTML,
12
+ ".md": MARKDOWN,
13
+ ".markdown": MARKDOWN,
14
+ }
15
+
16
+
17
+ class UnsupportedFile(Exception):
18
+ pass
19
+
20
+
21
+ def content_type_of(path: Path) -> str:
22
+ try:
23
+ return CONTENT_TYPES[path.suffix.lower()]
24
+ except KeyError:
25
+ raise UnsupportedFile(
26
+ f"Don't know how to publish {path.suffix or path.name!r} — "
27
+ f"expected one of {', '.join(sorted(CONTENT_TYPES))}."
28
+ ) from None
29
+
30
+
31
+ def slug_of(path: Path) -> str:
32
+ """A slug the server will accept: lowercase, digits and hyphens."""
33
+ slug = re.sub(r"[^a-z0-9]+", "-", path.stem.lower()).strip("-")
34
+ return slug[:63].rstrip("-")
35
+
36
+
37
+ def _title_from_html(html: str) -> str | None:
38
+ match = re.search(r"<title[^>]*>(.*?)</title>", html, re.IGNORECASE | re.DOTALL)
39
+ return match.group(1).strip() if match else None
40
+
41
+
42
+ def _title_from_markdown(md: str) -> str | None:
43
+ """The YAML frontmatter title if there is one, else the first H1."""
44
+ frontmatter = re.match(r"\A---\s*\n(.*?)\n---\s*\n", md, re.DOTALL)
45
+ if frontmatter:
46
+ match = re.search(r"^title:\s*(.+?)\s*$", frontmatter.group(1), re.MULTILINE)
47
+ if match:
48
+ return match.group(1).strip("\"'")
49
+ md = md[frontmatter.end() :]
50
+ match = re.search(r"^#\s+(.+?)\s*$", md, re.MULTILINE)
51
+ return match.group(1) if match else None
52
+
53
+
54
+ def title_of(content: str, content_type: str) -> str | None:
55
+ if content_type == HTML:
56
+ return _title_from_html(content)
57
+ return _title_from_markdown(content)
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: docdrop
3
+ Version: 0.1.0
4
+ Summary: Publish HTML and Markdown documents to DocDrop by unguessable link.
5
+ Project-URL: Homepage, https://docdrop.anvil.app
6
+ Project-URL: Repository, https://github.com/Empiria/docdrop
7
+ Project-URL: Documentation, https://github.com/Empiria/docdrop#readme
8
+ Author-email: Owen Campbell <directors@empiria.co.uk>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: markdown,publishing,sharing
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: httpx>=0.27
19
+ Description-Content-Type: text/markdown
20
+
21
+ # docdrop
22
+
23
+ `docdrop` is the command-line publisher for [docdrop][repo] — it uploads HTML
24
+ and Markdown documents to a docdrop server and gives you back an unguessable
25
+ link to share.
26
+
27
+ ```bash
28
+ pip install docdrop
29
+ ```
30
+
31
+ Full documentation — installation, CLI configuration and precedence, the
32
+ complete command reference, and how the server side works — lives in the main
33
+ README:
34
+
35
+ **https://github.com/Empiria/docdrop#readme**
36
+
37
+ [repo]: https://github.com/Empiria/docdrop
@@ -0,0 +1,11 @@
1
+ docdrop/__init__.py,sha256=VltBNhX4HweKSCeAoEx_mKJHUhHuhQXZFEuOEZAEhRM,46
2
+ docdrop/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
+ docdrop/cli.py,sha256=dRhlJaN35VsztrMYMW8lswVzGobb0QO8ftJ_IZL8dTE,9986
4
+ docdrop/client.py,sha256=blNhe6ddITVSEhIveOV4TsAENI5CVdY1sSAHc7OU8O0,3381
5
+ docdrop/config.py,sha256=788ZAPhvmI6fkbkITeEZo0d9RGWKD3F5ZJgQuXV9UVk,3990
6
+ docdrop/documents.py,sha256=by1n6M2edTWh6bm_ftJInE9qZnkXEH31IxenM2G5yh0,1694
7
+ docdrop-0.1.0.dist-info/METADATA,sha256=6TMWo8EkeYHBSb0Jy07ZqlFoWal_1GIw9kN1Bi2tu5c,1217
8
+ docdrop-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
9
+ docdrop-0.1.0.dist-info/entry_points.txt,sha256=u_X2kVvr_hD-Y0uvtFSVjT3CSw522RsrRNwGmkXxJb8,45
10
+ docdrop-0.1.0.dist-info/licenses/LICENSE,sha256=dAQo_2EefmwLw20I7lbtrUbx530Zst62UZVnxKlDv3s,1070
11
+ docdrop-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ docdrop = docdrop.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Owen Campbell
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
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell 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
13
+ all 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
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.