dcoink 0.1.0__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,11 @@
1
+ # 忽略内部规划文件,不提交到 GitHub
2
+ roadmap.md
3
+ API_REFERENCE.md
4
+
5
+ # 常见开发环境忽略
6
+ node_modules/
7
+ dist/
8
+ .env
9
+ .DS_Store
10
+ __pycache__/
11
+ *.pyc
dcoink-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: dcoink
3
+ Version: 0.1.0
4
+ Summary: The official Python SDK for dco.ink - A minimalist and developer-friendly URL shortener
5
+ Project-URL: Homepage, https://dco.ink
6
+ Project-URL: Source, https://github.com/dco/dco-ink-tools
7
+ Author-email: "dco.ink" <hello@dco.ink>
8
+ License-Expression: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Requires-Dist: httpx>=0.24.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # dcoink - Official Python SDK for dco.ink
17
+
18
+ The official, beautifully typed Python SDK for [dco.ink](https://dco.ink) - the minimalist, privacy-first URL shortener.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install dcoink
24
+ ```
25
+
26
+ ## Quick Start (Anonymous Mode)
27
+ You don't even need an account to start shortening URLs! The SDK provides a blazing-fast anonymous mode.
28
+
29
+ ```python
30
+ import dcoink
31
+
32
+ link = dcoink.shorten("https://example.com/a-very-long-url-that-needs-shortening")
33
+
34
+ print(f"Short URL: {link.short_url}")
35
+ # Output: https://dco.ink/xyz123
36
+ ```
37
+
38
+ ## Authenticated Mode
39
+ By passing your API Key, you can manage your links and access advanced features (like specifying custom short codes).
40
+
41
+ ```python
42
+ import dcoink
43
+
44
+ link = dcoink.shorten(
45
+ "https://example.com/my-campaign",
46
+ custom_code="mybrand",
47
+ api_key="dco_xxxxxx"
48
+ )
49
+
50
+ print(link.short_url)
51
+ # Output: https://dco.ink/mybrand
52
+ ```
53
+
54
+ ## Advanced Usage (Client API)
55
+
56
+ For robust applications, use the `Client` to manage your links, check history, and more.
57
+
58
+ ```python
59
+ from dcoink import Client
60
+
61
+ with Client(api_key="dco_xxxxxx") as client:
62
+ # Get user info
63
+ me = client.get_me()
64
+ print(f"Logged in as {me.name}")
65
+
66
+ # Create a link
67
+ link = client.create_link("https://github.com", custom_code="git")
68
+
69
+ # List history
70
+ links = client.list_links(limit=10)
71
+ for l in links:
72
+ print(l.short_url, l.clicks)
73
+
74
+ # Update and delete
75
+ client.update_link("git", "https://github.com/new-target")
76
+ client.delete_link("git")
77
+ ```
78
+
79
+ ## Async Support
80
+
81
+ Building high-concurrency scrapers or bots? We've got you covered with `AsyncClient` powered by `httpx`.
82
+
83
+ ```python
84
+ import asyncio
85
+ from dcoink import AsyncClient
86
+
87
+ async def main():
88
+ async with AsyncClient(api_key="dco_xxxxxx") as client:
89
+ link = await client.create_link("https://example.com")
90
+ print(link.short_url)
91
+
92
+ asyncio.run(main())
93
+ ```
94
+
95
+ ## Built-in CLI Tool
96
+ When you install the SDK, you also get a neat terminal command!
97
+
98
+ ```bash
99
+ # Quick shorten
100
+ dcoink shorten https://example.com
101
+
102
+ # Custom short code
103
+ dcoink shorten https://example.com -c mybrand -k dco_xxxxxx
104
+ ```
dcoink-0.1.0/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # dcoink - Official Python SDK for dco.ink
2
+
3
+ The official, beautifully typed Python SDK for [dco.ink](https://dco.ink) - the minimalist, privacy-first URL shortener.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install dcoink
9
+ ```
10
+
11
+ ## Quick Start (Anonymous Mode)
12
+ You don't even need an account to start shortening URLs! The SDK provides a blazing-fast anonymous mode.
13
+
14
+ ```python
15
+ import dcoink
16
+
17
+ link = dcoink.shorten("https://example.com/a-very-long-url-that-needs-shortening")
18
+
19
+ print(f"Short URL: {link.short_url}")
20
+ # Output: https://dco.ink/xyz123
21
+ ```
22
+
23
+ ## Authenticated Mode
24
+ By passing your API Key, you can manage your links and access advanced features (like specifying custom short codes).
25
+
26
+ ```python
27
+ import dcoink
28
+
29
+ link = dcoink.shorten(
30
+ "https://example.com/my-campaign",
31
+ custom_code="mybrand",
32
+ api_key="dco_xxxxxx"
33
+ )
34
+
35
+ print(link.short_url)
36
+ # Output: https://dco.ink/mybrand
37
+ ```
38
+
39
+ ## Advanced Usage (Client API)
40
+
41
+ For robust applications, use the `Client` to manage your links, check history, and more.
42
+
43
+ ```python
44
+ from dcoink import Client
45
+
46
+ with Client(api_key="dco_xxxxxx") as client:
47
+ # Get user info
48
+ me = client.get_me()
49
+ print(f"Logged in as {me.name}")
50
+
51
+ # Create a link
52
+ link = client.create_link("https://github.com", custom_code="git")
53
+
54
+ # List history
55
+ links = client.list_links(limit=10)
56
+ for l in links:
57
+ print(l.short_url, l.clicks)
58
+
59
+ # Update and delete
60
+ client.update_link("git", "https://github.com/new-target")
61
+ client.delete_link("git")
62
+ ```
63
+
64
+ ## Async Support
65
+
66
+ Building high-concurrency scrapers or bots? We've got you covered with `AsyncClient` powered by `httpx`.
67
+
68
+ ```python
69
+ import asyncio
70
+ from dcoink import AsyncClient
71
+
72
+ async def main():
73
+ async with AsyncClient(api_key="dco_xxxxxx") as client:
74
+ link = await client.create_link("https://example.com")
75
+ print(link.short_url)
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ ## Built-in CLI Tool
81
+ When you install the SDK, you also get a neat terminal command!
82
+
83
+ ```bash
84
+ # Quick shorten
85
+ dcoink shorten https://example.com
86
+
87
+ # Custom short code
88
+ dcoink shorten https://example.com -c mybrand -k dco_xxxxxx
89
+ ```
@@ -0,0 +1,52 @@
1
+ """
2
+ dcoink - The official Python SDK for dco.ink
3
+ """
4
+
5
+ import httpx
6
+ from typing import Optional
7
+
8
+ from .models import Link, UserInfo
9
+ from .client import Client, BASE_URL
10
+ from .async_client import AsyncClient
11
+ from . import errors
12
+
13
+ def shorten(url: str, custom_code: Optional[str] = None, api_key: Optional[str] = None, base_url: str = BASE_URL) -> Link:
14
+ """
15
+ Intelligently shortens a URL.
16
+ - If `api_key` is provided, calls POST /api/links (authenticated, allows custom codes).
17
+ - If `api_key` is NOT provided, calls GET /api/s (fast anonymous mode).
18
+ """
19
+ if custom_code and not api_key:
20
+ raise errors.ValidationError("custom_code requires an api_key to be provided.")
21
+
22
+ if api_key:
23
+ # Authenticated Mode
24
+ with Client(api_key=api_key, base_url=base_url) as client:
25
+ return client.create_link(url, custom_code=custom_code)
26
+ else:
27
+ # Fast Anonymous Mode
28
+ response = httpx.get(f"{base_url}/api/s", params={"url": url})
29
+
30
+ if not response.is_success:
31
+ # Handle specific known errors from the plain-text endpoint
32
+ if response.text.startswith("error: invalid_url"):
33
+ raise errors.ValidationError("Please provide a valid URL.", status_code=response.status_code)
34
+ raise errors.DcoApiError(f"Failed to create anonymous link: {response.text}", status_code=response.status_code)
35
+
36
+ short_url = response.text.strip()
37
+ short_code = short_url.split("/")[-1]
38
+
39
+ return Link(
40
+ short_code=short_code,
41
+ short_url=short_url,
42
+ target_url=url
43
+ )
44
+
45
+ __all__ = [
46
+ "Client",
47
+ "AsyncClient",
48
+ "Link",
49
+ "UserInfo",
50
+ "shorten",
51
+ "errors",
52
+ ]
@@ -0,0 +1,113 @@
1
+ import httpx
2
+ from typing import List, Optional, Dict, Any
3
+ from .models import Link, UserInfo
4
+ from .errors import (
5
+ DcoApiError,
6
+ AuthenticationError,
7
+ ValidationError,
8
+ CodeTakenError,
9
+ ForbiddenError,
10
+ RateLimitError
11
+ )
12
+
13
+ BASE_URL = "https://api.dco.ink"
14
+
15
+ class AsyncClient:
16
+ """Asynchronous client for the dco.ink API."""
17
+
18
+ def __init__(self, api_key: str, base_url: str = BASE_URL, timeout: float = 10.0):
19
+ self.api_key = api_key
20
+ self.base_url = base_url
21
+ self._client = httpx.AsyncClient(
22
+ base_url=self.base_url,
23
+ headers={
24
+ "X-API-Key": self.api_key,
25
+ "Content-Type": "application/json",
26
+ "User-Agent": "dcoink-python-sdk/0.1.0"
27
+ },
28
+ timeout=timeout
29
+ )
30
+
31
+ def _handle_error(self, response: httpx.Response):
32
+ try:
33
+ data = response.json()
34
+ error_code = data.get("error", "unknown_error")
35
+ message = data.get("message", response.text)
36
+ except Exception:
37
+ error_code = "unknown_error"
38
+ message = response.text
39
+
40
+ status = response.status_code
41
+ if status == 401:
42
+ raise AuthenticationError(message, error_code, status)
43
+ elif status == 403:
44
+ raise ForbiddenError(message, error_code, status)
45
+ elif status == 409:
46
+ raise CodeTakenError(message, error_code, status)
47
+ elif status == 429:
48
+ raise RateLimitError("Rate limit exceeded", error_code, status)
49
+ elif status == 400:
50
+ raise ValidationError(message, error_code, status)
51
+ else:
52
+ raise DcoApiError(message, error_code, status)
53
+
54
+ async def _request(self, method: str, path: str, **kwargs) -> Any:
55
+ response = await self._client.request(method, path, **kwargs)
56
+ if not response.is_success:
57
+ self._handle_error(response)
58
+
59
+ if response.status_code == 204 or not response.content:
60
+ return None
61
+
62
+ return response.json()
63
+
64
+ async def get_me(self) -> UserInfo:
65
+ """Get the current authenticated user's details."""
66
+ data = await self._request("GET", "/api/auth/me")
67
+ return UserInfo.from_dict(data)
68
+
69
+ async def create_link(self, url: str, custom_code: Optional[str] = None) -> Link:
70
+ """Create a new shortened link."""
71
+ payload = {"url": url}
72
+ if custom_code:
73
+ payload["custom_code"] = custom_code
74
+
75
+ data = await self._request("POST", "/api/links", json=payload)
76
+ return Link(
77
+ short_code=data["short_code"],
78
+ short_url=data["short_url"],
79
+ target_url=data.get("target_url", url),
80
+ expires_at=data.get("expires_at")
81
+ )
82
+
83
+ async def list_links(self, limit: int = 50, offset: int = 0) -> List[Link]:
84
+ """List all links created by the current user."""
85
+ data = await self._request("GET", "/api/links", params={"limit": limit, "offset": offset})
86
+ links = []
87
+ for item in data.get("links", []):
88
+ links.append(Link(
89
+ short_code=item["short_code"],
90
+ short_url=f"https://dco.ink/{item['short_code']}",
91
+ target_url=item.get("target_url"),
92
+ created_at=item.get("created_at"),
93
+ expires_at=item.get("expires_at")
94
+ ))
95
+ return links
96
+
97
+ async def update_link(self, short_code: str, new_url: str) -> None:
98
+ """Update the target URL of an existing link."""
99
+ await self._request("PUT", f"/api/links/{short_code}", json={"url": new_url})
100
+
101
+ async def delete_link(self, short_code: str) -> None:
102
+ """Delete an existing link."""
103
+ await self._request("DELETE", f"/api/links/{short_code}")
104
+
105
+ async def close(self):
106
+ """Close the underlying HTTP client."""
107
+ await self._client.aclose()
108
+
109
+ async def __aenter__(self):
110
+ return self
111
+
112
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
113
+ await self.close()
@@ -0,0 +1,34 @@
1
+ import argparse
2
+ import sys
3
+ import dcoink
4
+
5
+ def main():
6
+ parser = argparse.ArgumentParser(description="dco.ink - Minimalist URL Shortener CLI")
7
+ subparsers = parser.add_subparsers(dest="command")
8
+
9
+ # shorten subcommand
10
+ shorten_parser = subparsers.add_parser("shorten", help="Shorten a URL")
11
+ shorten_parser.add_argument("url", help="The long URL to shorten")
12
+ shorten_parser.add_argument("-c", "--code", help="Custom short code (requires API Key)", default=None)
13
+ shorten_parser.add_argument("-k", "--key", help="Your dco.ink API Key (optional for random links)", default=None)
14
+
15
+ args = parser.parse_args()
16
+
17
+ if args.command is None:
18
+ parser.print_help()
19
+ sys.exit(1)
20
+
21
+ try:
22
+ link = dcoink.shorten(url=args.url, custom_code=args.code, api_key=args.key)
23
+ print(f"✅ Success! Your short link is ready:")
24
+ print(f"👉 {link.short_url}")
25
+ except dcoink.errors.DcoApiError as e:
26
+ print(f"❌ API Error: {e.message}")
27
+ sys.exit(1)
28
+ except Exception as e:
29
+ print(f"❌ Unexpected Error: {str(e)}")
30
+ sys.exit(1)
31
+
32
+ if __name__ == "__main__":
33
+ main()
34
+
@@ -0,0 +1,114 @@
1
+ import httpx
2
+ from typing import List, Optional, Dict, Any
3
+ from .models import Link, UserInfo
4
+ from .errors import (
5
+ DcoApiError,
6
+ AuthenticationError,
7
+ ValidationError,
8
+ CodeTakenError,
9
+ ForbiddenError,
10
+ RateLimitError
11
+ )
12
+
13
+ BASE_URL = "https://api.dco.ink"
14
+
15
+ class Client:
16
+ """Synchronous client for the dco.ink API."""
17
+
18
+ def __init__(self, api_key: str, base_url: str = BASE_URL, timeout: float = 10.0):
19
+ self.api_key = api_key
20
+ self.base_url = base_url
21
+ self._client = httpx.Client(
22
+ base_url=self.base_url,
23
+ headers={
24
+ "X-API-Key": self.api_key,
25
+ "Content-Type": "application/json",
26
+ "User-Agent": "dcoink-python-sdk/0.1.0"
27
+ },
28
+ timeout=timeout
29
+ )
30
+
31
+ def _handle_error(self, response: httpx.Response):
32
+ try:
33
+ data = response.json()
34
+ error_code = data.get("error", "unknown_error")
35
+ message = data.get("message", response.text)
36
+ except Exception:
37
+ error_code = "unknown_error"
38
+ message = response.text
39
+
40
+ status = response.status_code
41
+ if status == 401:
42
+ raise AuthenticationError(message, error_code, status)
43
+ elif status == 403:
44
+ raise ForbiddenError(message, error_code, status)
45
+ elif status == 409:
46
+ raise CodeTakenError(message, error_code, status)
47
+ elif status == 429:
48
+ raise RateLimitError("Rate limit exceeded", error_code, status)
49
+ elif status == 400:
50
+ raise ValidationError(message, error_code, status)
51
+ else:
52
+ raise DcoApiError(message, error_code, status)
53
+
54
+ def _request(self, method: str, path: str, **kwargs) -> Any:
55
+ response = self._client.request(method, path, **kwargs)
56
+ if not response.is_success:
57
+ self._handle_error(response)
58
+
59
+ # 204 No Content handling
60
+ if response.status_code == 204 or not response.content:
61
+ return None
62
+
63
+ return response.json()
64
+
65
+ def get_me(self) -> UserInfo:
66
+ """Get the current authenticated user's details."""
67
+ data = self._request("GET", "/api/auth/me")
68
+ return UserInfo.from_dict(data)
69
+
70
+ def create_link(self, url: str, custom_code: Optional[str] = None) -> Link:
71
+ """Create a new shortened link."""
72
+ payload = {"url": url}
73
+ if custom_code:
74
+ payload["custom_code"] = custom_code
75
+
76
+ data = self._request("POST", "/api/links", json=payload)
77
+ return Link(
78
+ short_code=data["short_code"],
79
+ short_url=data["short_url"],
80
+ target_url=data.get("target_url", url),
81
+ expires_at=data.get("expires_at")
82
+ )
83
+
84
+ def list_links(self, limit: int = 50, offset: int = 0) -> List[Link]:
85
+ """List all links created by the current user."""
86
+ data = self._request("GET", "/api/links", params={"limit": limit, "offset": offset})
87
+ links = []
88
+ for item in data.get("links", []):
89
+ links.append(Link(
90
+ short_code=item["short_code"],
91
+ short_url=f"https://dco.ink/{item['short_code']}",
92
+ target_url=item.get("target_url"),
93
+ created_at=item.get("created_at"),
94
+ expires_at=item.get("expires_at")
95
+ ))
96
+ return links
97
+
98
+ def update_link(self, short_code: str, new_url: str) -> None:
99
+ """Update the target URL of an existing link."""
100
+ self._request("PUT", f"/api/links/{short_code}", json={"url": new_url})
101
+
102
+ def delete_link(self, short_code: str) -> None:
103
+ """Delete an existing link."""
104
+ self._request("DELETE", f"/api/links/{short_code}")
105
+
106
+ def close(self):
107
+ """Close the underlying HTTP client."""
108
+ self._client.close()
109
+
110
+ def __enter__(self):
111
+ return self
112
+
113
+ def __exit__(self, exc_type, exc_val, exc_tb):
114
+ self.close()
@@ -0,0 +1,29 @@
1
+ from typing import Optional
2
+
3
+ class DcoApiError(Exception):
4
+ """Base class for all API errors."""
5
+ def __init__(self, message: str, code: Optional[str] = None, status_code: Optional[int] = None):
6
+ super().__init__(message)
7
+ self.message = message
8
+ self.code = code
9
+ self.status_code = status_code
10
+
11
+ class AuthenticationError(DcoApiError):
12
+ """Raised when the API key is missing or invalid."""
13
+ pass
14
+
15
+ class ValidationError(DcoApiError):
16
+ """Raised when the provided URL or custom code is invalid."""
17
+ pass
18
+
19
+ class CodeTakenError(DcoApiError):
20
+ """Raised when the requested custom code is already in use."""
21
+ pass
22
+
23
+ class ForbiddenError(DcoApiError):
24
+ """Raised when the user attempts an action they do not have permission for."""
25
+ pass
26
+
27
+ class RateLimitError(DcoApiError):
28
+ """Raised when the API rate limit is exceeded."""
29
+ pass
@@ -0,0 +1,35 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional, List
3
+
4
+ @dataclass
5
+ class Link:
6
+ """Represents a shortened link."""
7
+ short_code: str
8
+ short_url: str
9
+ target_url: Optional[str] = None
10
+ id: Optional[str] = None
11
+ clicks: int = 0
12
+ created_at: Optional[str] = None
13
+ expires_at: Optional[str] = None
14
+
15
+ @classmethod
16
+ def from_dict(cls, data: dict) -> 'Link':
17
+ valid_keys = {k for k in cls.__annotations__}
18
+ filtered = {k: v for k, v in data.items() if k in valid_keys}
19
+ return cls(**filtered)
20
+
21
+ @dataclass
22
+ class UserInfo:
23
+ """Represents the authenticated user's details."""
24
+ id: str
25
+ email: str
26
+ name: str
27
+ is_subscribed: int
28
+ api_token: str
29
+ avatar_url: Optional[str] = None
30
+
31
+ @classmethod
32
+ def from_dict(cls, data: dict) -> 'UserInfo':
33
+ valid_keys = {k for k in cls.__annotations__}
34
+ filtered = {k: v for k, v in data.items() if k in valid_keys}
35
+ return cls(**filtered)
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dcoink"
7
+ version = "0.1.0"
8
+ description = "The official Python SDK for dco.ink - A minimalist and developer-friendly URL shortener"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "dco.ink", email = "hello@dco.ink" },
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+ dependencies = [
21
+ "httpx>=0.24.0"
22
+ ]
23
+
24
+ [project.urls]
25
+ "Homepage" = "https://dco.ink"
26
+ "Source" = "https://github.com/dco/dco-ink-tools"
27
+
28
+ [project.scripts]
29
+ dcoink = "dcoink.cli:main"
@@ -0,0 +1,36 @@
1
+ import asyncio
2
+ import os
3
+ import dcoink
4
+
5
+ # User provided testing token
6
+ TEST_TOKEN = os.getenv("DCO_API_KEY", "your_api_key_here")
7
+
8
+ def test_sync():
9
+ print("--- 1. Testing Sync Anonymous ---")
10
+ link = dcoink.shorten("https://google.com")
11
+ print(f"✅ Anonymous link generated: {link.short_url} (Code: {link.short_code})")
12
+
13
+ print("\n--- 2. Testing Sync Client (Authenticated) ---")
14
+ with dcoink.Client(api_key=TEST_TOKEN) as client:
15
+ # Get Me
16
+ me = client.get_me()
17
+ print(f"✅ User Info: {me.name} ({me.email}) - Subscribed: {me.is_subscribed}")
18
+
19
+ # Create link
20
+ link2 = client.create_link("https://github.com/dcoink")
21
+ print(f"✅ Authenticated link generated: {link2.short_url}")
22
+
23
+ # List links
24
+ links = client.list_links(limit=5)
25
+ print(f"✅ Found {len(links)} links in history.")
26
+
27
+ async def test_async():
28
+ print("\n--- 3. Testing Async Client (Authenticated) ---")
29
+ async with dcoink.AsyncClient(api_key=TEST_TOKEN) as client:
30
+ me = await client.get_me()
31
+ print(f"✅ Async User Info: {me.name} ({me.email})")
32
+
33
+ if __name__ == "__main__":
34
+ test_sync()
35
+ asyncio.run(test_async())
36
+ print("\n🎉 All SDK tests passed!")