typekit2 1.0.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.
- typekit2/__init__.py +14 -0
- typekit2/__main__.py +144 -0
- typekit2/__version__.py +3 -0
- typekit2/client.py +246 -0
- typekit2/exceptions.py +29 -0
- typekit2-1.0.0.dist-info/METADATA +161 -0
- typekit2-1.0.0.dist-info/RECORD +10 -0
- typekit2-1.0.0.dist-info/WHEEL +4 -0
- typekit2-1.0.0.dist-info/entry_points.txt +2 -0
- typekit2-1.0.0.dist-info/licenses/LICENSE +21 -0
typekit2/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# this_file: typekit2/__init__.py
|
|
2
|
+
"""Public API for the typekit2 package."""
|
|
3
|
+
|
|
4
|
+
from .__version__ import __version__
|
|
5
|
+
from .client import Typekit
|
|
6
|
+
from .exceptions import TypekitAPIError, TypekitConfigurationError, TypekitError
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Typekit",
|
|
10
|
+
"TypekitAPIError",
|
|
11
|
+
"TypekitConfigurationError",
|
|
12
|
+
"TypekitError",
|
|
13
|
+
"__version__",
|
|
14
|
+
]
|
typekit2/__main__.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# this_file: typekit2/__main__.py
|
|
2
|
+
"""Fire-powered command-line interface for typekit2."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import fire
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
from .client import Typekit
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_csv(value: str | Sequence[str]) -> list[str]:
|
|
18
|
+
"""Parse a comma-separated Fire argument or an existing sequence."""
|
|
19
|
+
items = value.split(",") if isinstance(value, str) else list(value)
|
|
20
|
+
parsed = [str(item).strip() for item in items if str(item).strip()]
|
|
21
|
+
if not parsed:
|
|
22
|
+
raise ValueError("expected at least one value")
|
|
23
|
+
return parsed
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_families(value: str | list[dict[str, Any]] | None) -> list[dict[str, Any]] | None:
|
|
27
|
+
"""Parse a JSON list accepted by create-kit and update-kit."""
|
|
28
|
+
if value is None or isinstance(value, list):
|
|
29
|
+
return value
|
|
30
|
+
parsed = json.loads(value)
|
|
31
|
+
if not isinstance(parsed, list) or not all(isinstance(item, dict) for item in parsed):
|
|
32
|
+
raise ValueError("families must be a JSON list of objects")
|
|
33
|
+
return parsed
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TypekitCLI:
|
|
37
|
+
"""Read and manage Adobe Fonts kits and font metadata."""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def client(self) -> Typekit:
|
|
41
|
+
"""Create the API client lazily so help and doctor work without credentials."""
|
|
42
|
+
return Typekit()
|
|
43
|
+
|
|
44
|
+
def doctor(self) -> dict[str, str]:
|
|
45
|
+
"""Check whether TYPEKIT_API_KEY is available without revealing it."""
|
|
46
|
+
load_dotenv()
|
|
47
|
+
configured = bool(os.getenv("TYPEKIT_API_KEY", "").strip())
|
|
48
|
+
return {
|
|
49
|
+
"api_key": "configured" if configured else "missing",
|
|
50
|
+
"source": "environment-or-dotenv" if configured else "none",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def kits(self) -> list[dict[str, Any]]:
|
|
54
|
+
"""List kits owned by the authenticated user."""
|
|
55
|
+
return self.client.list_kits()
|
|
56
|
+
|
|
57
|
+
def kit(self, kit_id: str, published: bool = False) -> dict[str, Any]:
|
|
58
|
+
"""Get a draft kit or its published version."""
|
|
59
|
+
return self.client.get_kit(kit_id, published=published)
|
|
60
|
+
|
|
61
|
+
def family(self, family: str) -> dict[str, Any]:
|
|
62
|
+
"""Get a font family by ID or slug."""
|
|
63
|
+
return self.client.get_font_family(family)
|
|
64
|
+
|
|
65
|
+
def variations(self, family: str) -> list[str]:
|
|
66
|
+
"""List FVD variation codes for a font family."""
|
|
67
|
+
return self.client.get_font_variations(family)
|
|
68
|
+
|
|
69
|
+
def libraries(self) -> list[dict[str, Any]]:
|
|
70
|
+
"""List font libraries."""
|
|
71
|
+
return self.client.list_libraries()
|
|
72
|
+
|
|
73
|
+
def library(self, library: str, page: int = 1, per_page: int = 100) -> dict[str, Any]:
|
|
74
|
+
"""Get one paginated font library."""
|
|
75
|
+
return self.client.get_library(library, page=page, per_page=per_page)
|
|
76
|
+
|
|
77
|
+
def create_kit(
|
|
78
|
+
self,
|
|
79
|
+
name: str,
|
|
80
|
+
domains: str,
|
|
81
|
+
families: str | None = None,
|
|
82
|
+
segmented_css_names: bool | None = None,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Create a draft kit; domains are CSV and families are a JSON list."""
|
|
85
|
+
return self.client.create_kit(
|
|
86
|
+
name,
|
|
87
|
+
parse_csv(domains),
|
|
88
|
+
parse_families(families),
|
|
89
|
+
segmented_css_names,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def update_kit(
|
|
93
|
+
self,
|
|
94
|
+
kit_id: str,
|
|
95
|
+
name: str | None = None,
|
|
96
|
+
domains: str | None = None,
|
|
97
|
+
families: str | None = None,
|
|
98
|
+
segmented_css_names: bool | None = None,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
"""Update supplied draft-kit fields."""
|
|
101
|
+
return self.client.update_kit(
|
|
102
|
+
kit_id,
|
|
103
|
+
name=name,
|
|
104
|
+
domains=parse_csv(domains) if domains is not None else None,
|
|
105
|
+
families=parse_families(families),
|
|
106
|
+
segmented_css_names=segmented_css_names,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def remove_kit(self, kit_id: str) -> dict[str, Any]:
|
|
110
|
+
"""Delete a kit. This is a live destructive action."""
|
|
111
|
+
return self.client.remove_kit(kit_id)
|
|
112
|
+
|
|
113
|
+
def publish_kit(self, kit_id: str) -> dict[str, Any]:
|
|
114
|
+
"""Publish the current draft kit to the CDN."""
|
|
115
|
+
return self.client.publish_kit(kit_id)
|
|
116
|
+
|
|
117
|
+
def add_font(
|
|
118
|
+
self,
|
|
119
|
+
kit_id: str,
|
|
120
|
+
family: str,
|
|
121
|
+
variations: str | None = None,
|
|
122
|
+
subset: str = "default",
|
|
123
|
+
) -> dict[str, Any]:
|
|
124
|
+
"""Add or replace one font family in a draft kit."""
|
|
125
|
+
parsed = parse_csv(variations) if variations is not None else None
|
|
126
|
+
return self.client.add_font(kit_id, family, parsed, subset)
|
|
127
|
+
|
|
128
|
+
def remove_font(self, kit_id: str, family: str) -> dict[str, Any]:
|
|
129
|
+
"""Remove one font family from a draft kit."""
|
|
130
|
+
return self.client.remove_font(kit_id, family)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _serialize(value: Any) -> str:
|
|
134
|
+
"""Produce stable JSON for command results."""
|
|
135
|
+
return json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main() -> None:
|
|
139
|
+
"""Run the typekit2 CLI."""
|
|
140
|
+
fire.Fire(TypekitCLI, name="typekit2", serialize=_serialize)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|
typekit2/__version__.py
ADDED
typekit2/client.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# this_file: typekit2/client.py
|
|
2
|
+
"""Adobe Fonts (Typekit) API client."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
from .__version__ import __version__
|
|
15
|
+
from .exceptions import TypekitAPIError, TypekitConfigurationError
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE_URL = "https://typekit.com/api/v1/json"
|
|
18
|
+
DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
FormData = list[tuple[str, str]]
|
|
21
|
+
Family = Mapping[str, Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _as_list(value: str | Sequence[str], field: str) -> list[str]:
|
|
25
|
+
"""Parse a non-empty string or sequence at the public API boundary."""
|
|
26
|
+
values = [value] if isinstance(value, str) else list(value)
|
|
27
|
+
if not values or not all(isinstance(item, str) and item.strip() for item in values):
|
|
28
|
+
raise ValueError(f"{field} must contain at least one non-empty string")
|
|
29
|
+
return [item.strip() for item in values]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _kit_form(
|
|
33
|
+
*,
|
|
34
|
+
name: str | None = None,
|
|
35
|
+
domains: str | Sequence[str] | None = None,
|
|
36
|
+
families: Sequence[Family] | None = None,
|
|
37
|
+
segmented_css_names: bool | None = None,
|
|
38
|
+
) -> FormData:
|
|
39
|
+
"""Encode documented Rails-style kit form parameters."""
|
|
40
|
+
data: FormData = []
|
|
41
|
+
if name is not None:
|
|
42
|
+
if not name.strip():
|
|
43
|
+
raise ValueError("name must not be empty")
|
|
44
|
+
data.append(("name", name))
|
|
45
|
+
if domains is not None:
|
|
46
|
+
data.extend(("domains[]", domain) for domain in _as_list(domains, "domains"))
|
|
47
|
+
if families is not None:
|
|
48
|
+
for index, family in enumerate(families):
|
|
49
|
+
family_id = family.get("id")
|
|
50
|
+
if not isinstance(family_id, str) or not family_id.strip():
|
|
51
|
+
raise ValueError(f"families[{index}].id must be a non-empty string")
|
|
52
|
+
prefix = f"families[{index}]"
|
|
53
|
+
data.append((f"{prefix}[id]", family_id.strip()))
|
|
54
|
+
subset = family.get("subset")
|
|
55
|
+
if subset is not None:
|
|
56
|
+
if subset not in {"default", "all"}:
|
|
57
|
+
raise ValueError(f"families[{index}].subset must be 'default' or 'all'")
|
|
58
|
+
data.append((f"{prefix}[subset]", subset))
|
|
59
|
+
variations = family.get("variations")
|
|
60
|
+
if variations is not None:
|
|
61
|
+
data.append((f"{prefix}[variations]", ",".join(_as_list(variations, "variations"))))
|
|
62
|
+
if segmented_css_names is not None:
|
|
63
|
+
data.append(("segmented_css_names", str(segmented_css_names).lower()))
|
|
64
|
+
return data
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Typekit:
|
|
68
|
+
"""Client for the Adobe Fonts API documented at fonts.adobe.com/docs/api."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
api_key: str | None = None,
|
|
73
|
+
*,
|
|
74
|
+
api_token: str | None = None,
|
|
75
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
76
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
77
|
+
session: requests.Session | Any | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
load_dotenv()
|
|
80
|
+
key = api_key or api_token or os.getenv("TYPEKIT_API_KEY")
|
|
81
|
+
if not key or not key.strip():
|
|
82
|
+
raise TypekitConfigurationError(
|
|
83
|
+
"Set TYPEKIT_API_KEY in the environment or .env, or pass api_key explicitly"
|
|
84
|
+
)
|
|
85
|
+
if not base_url.startswith("https://"):
|
|
86
|
+
raise TypekitConfigurationError("Authenticated API requests require an HTTPS base URL")
|
|
87
|
+
if timeout <= 0:
|
|
88
|
+
raise ValueError("timeout must be greater than zero")
|
|
89
|
+
self.api_key = key.strip()
|
|
90
|
+
self.base_url = base_url.rstrip("/")
|
|
91
|
+
self.timeout = timeout
|
|
92
|
+
self.session = session or requests.Session()
|
|
93
|
+
|
|
94
|
+
def request(
|
|
95
|
+
self,
|
|
96
|
+
method: str,
|
|
97
|
+
path: str,
|
|
98
|
+
*,
|
|
99
|
+
data: FormData | None = None,
|
|
100
|
+
params: Mapping[str, Any] | None = None,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""Make an authenticated request and return the decoded JSON object."""
|
|
103
|
+
kwargs: dict[str, Any] = {
|
|
104
|
+
"headers": {
|
|
105
|
+
"User-Agent": f"typekit2/{__version__}",
|
|
106
|
+
"X-Typekit-Token": self.api_key,
|
|
107
|
+
},
|
|
108
|
+
"timeout": self.timeout,
|
|
109
|
+
}
|
|
110
|
+
if data is not None:
|
|
111
|
+
kwargs["data"] = data
|
|
112
|
+
if params is not None:
|
|
113
|
+
kwargs["params"] = dict(params)
|
|
114
|
+
try:
|
|
115
|
+
response = self.session.request(
|
|
116
|
+
method.upper(), f"{self.base_url}/{path.lstrip('/')}", **kwargs
|
|
117
|
+
)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
payload = response.json()
|
|
120
|
+
except requests.RequestException as error:
|
|
121
|
+
status = getattr(getattr(error, "response", None), "status_code", None)
|
|
122
|
+
raise TypekitAPIError(
|
|
123
|
+
f"Adobe Fonts request failed: {error}", status_code=status
|
|
124
|
+
) from error
|
|
125
|
+
except ValueError as error:
|
|
126
|
+
raise TypekitAPIError("Adobe Fonts returned invalid JSON") from error
|
|
127
|
+
if not isinstance(payload, dict):
|
|
128
|
+
raise TypekitAPIError("Adobe Fonts returned a non-object JSON response")
|
|
129
|
+
errors = payload.get("errors")
|
|
130
|
+
if errors:
|
|
131
|
+
normalized = errors if isinstance(errors, list) else [errors]
|
|
132
|
+
raise TypekitAPIError("Adobe Fonts API returned errors", errors=normalized)
|
|
133
|
+
return payload
|
|
134
|
+
|
|
135
|
+
def list_kits(self) -> list[dict[str, Any]]:
|
|
136
|
+
"""Return kits owned by the authenticated user."""
|
|
137
|
+
return self.request("GET", "kits").get("kits", [])
|
|
138
|
+
|
|
139
|
+
def get_kit(self, kit_id: str, *, published: bool = False) -> dict[str, Any]:
|
|
140
|
+
"""Return a draft kit, or its published version when requested."""
|
|
141
|
+
suffix = "/published" if published else ""
|
|
142
|
+
return self.request("GET", f"kits/{quote(kit_id, safe='')}{suffix}")
|
|
143
|
+
|
|
144
|
+
def get_published_kit(self, kit_id: str) -> dict[str, Any]:
|
|
145
|
+
"""Return the version of a kit currently published to the CDN."""
|
|
146
|
+
return self.get_kit(kit_id, published=True)
|
|
147
|
+
|
|
148
|
+
def create_kit(
|
|
149
|
+
self,
|
|
150
|
+
name: str,
|
|
151
|
+
domains: str | Sequence[str],
|
|
152
|
+
families: Sequence[Family] | None = None,
|
|
153
|
+
segmented_css_names: bool | None = None,
|
|
154
|
+
) -> dict[str, Any]:
|
|
155
|
+
"""Create a draft kit."""
|
|
156
|
+
data = _kit_form(
|
|
157
|
+
name=name,
|
|
158
|
+
domains=domains,
|
|
159
|
+
families=families,
|
|
160
|
+
segmented_css_names=segmented_css_names,
|
|
161
|
+
)
|
|
162
|
+
return self.request("POST", "kits", data=data)
|
|
163
|
+
|
|
164
|
+
def update_kit(
|
|
165
|
+
self,
|
|
166
|
+
kit_id: str,
|
|
167
|
+
name: str | None = None,
|
|
168
|
+
domains: str | Sequence[str] | None = None,
|
|
169
|
+
families: Sequence[Family] | None = None,
|
|
170
|
+
segmented_css_names: bool | None = None,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""Replace only the supplied draft-kit attributes."""
|
|
173
|
+
data = _kit_form(
|
|
174
|
+
name=name,
|
|
175
|
+
domains=domains,
|
|
176
|
+
families=families,
|
|
177
|
+
segmented_css_names=segmented_css_names,
|
|
178
|
+
)
|
|
179
|
+
if not data:
|
|
180
|
+
raise ValueError("update_kit requires at least one field")
|
|
181
|
+
return self.request("POST", f"kits/{quote(kit_id, safe='')}", data=data)
|
|
182
|
+
|
|
183
|
+
def remove_kit(self, kit_id: str) -> dict[str, Any]:
|
|
184
|
+
"""Delete a kit."""
|
|
185
|
+
return self.request("DELETE", f"kits/{quote(kit_id, safe='')}")
|
|
186
|
+
|
|
187
|
+
def publish_kit(self, kit_id: str) -> dict[str, Any]:
|
|
188
|
+
"""Publish the current draft kit asynchronously."""
|
|
189
|
+
return self.request("POST", f"kits/{quote(kit_id, safe='')}/publish")
|
|
190
|
+
|
|
191
|
+
def get_font_family(self, family: str) -> dict[str, Any]:
|
|
192
|
+
"""Return a font family by ID or slug."""
|
|
193
|
+
return self.request("GET", f"families/{quote(family, safe='')}")
|
|
194
|
+
|
|
195
|
+
def get_font_variations(self, family: str) -> list[str]:
|
|
196
|
+
"""Return Font Variation Description values for a family."""
|
|
197
|
+
variations = self.get_font_family(family).get("family", {}).get("variations", [])
|
|
198
|
+
return [item["fvd"] for item in variations if isinstance(item, dict) and "fvd" in item]
|
|
199
|
+
|
|
200
|
+
def list_libraries(self) -> list[dict[str, Any]]:
|
|
201
|
+
"""Return available Adobe Fonts libraries."""
|
|
202
|
+
return self.request("GET", "libraries").get("libraries", [])
|
|
203
|
+
|
|
204
|
+
def get_library(self, library: str, *, page: int = 1, per_page: int = 100) -> dict[str, Any]:
|
|
205
|
+
"""Return a paginated font library."""
|
|
206
|
+
if page < 1 or per_page < 1:
|
|
207
|
+
raise ValueError("page and per_page must be positive integers")
|
|
208
|
+
return self.request(
|
|
209
|
+
"GET",
|
|
210
|
+
f"libraries/{quote(library, safe='')}",
|
|
211
|
+
params={"page": page, "per_page": per_page},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def add_font(
|
|
215
|
+
self,
|
|
216
|
+
kit_id: str,
|
|
217
|
+
family: str,
|
|
218
|
+
variations: str | Sequence[str] | None = None,
|
|
219
|
+
subset: str = "default",
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
"""Add or replace one font family in a draft kit."""
|
|
222
|
+
if subset not in {"default", "all"}:
|
|
223
|
+
raise ValueError("subset must be 'default' or 'all'")
|
|
224
|
+
data: FormData = [("subset", subset)]
|
|
225
|
+
if variations is not None:
|
|
226
|
+
data.append(("variations", ",".join(_as_list(variations, "variations"))))
|
|
227
|
+
path = f"kits/{quote(kit_id, safe='')}/families/{quote(family, safe='')}"
|
|
228
|
+
return self.request("POST", path, data=data)
|
|
229
|
+
|
|
230
|
+
def remove_font(self, kit_id: str, family: str) -> dict[str, Any]:
|
|
231
|
+
"""Remove one font family from a draft kit."""
|
|
232
|
+
path = f"kits/{quote(kit_id, safe='')}/families/{quote(family, safe='')}"
|
|
233
|
+
return self.request("DELETE", path)
|
|
234
|
+
|
|
235
|
+
def kit_contains_font(self, kit_id: str, family: str) -> bool:
|
|
236
|
+
"""Return whether the draft kit contains the resolved family ID."""
|
|
237
|
+
family_id = self.get_font_family(family).get("family", {}).get("id")
|
|
238
|
+
return family_id in self.get_kit_fonts(kit_id)
|
|
239
|
+
|
|
240
|
+
def get_kit_fonts(self, kit_id: str) -> list[str]:
|
|
241
|
+
"""Return family IDs in a draft kit."""
|
|
242
|
+
families = self.get_kit(kit_id).get("kit", {}).get("families", [])
|
|
243
|
+
return [item["id"] for item in families if isinstance(item, dict) and "id" in item]
|
|
244
|
+
|
|
245
|
+
kit_add_font = add_font
|
|
246
|
+
kit_remove_font = remove_font
|
typekit2/exceptions.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# this_file: typekit2/exceptions.py
|
|
2
|
+
"""Typed errors raised by typekit2."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TypekitError(Exception):
|
|
10
|
+
"""Base class for all package errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TypekitConfigurationError(TypekitError):
|
|
14
|
+
"""The client cannot be configured from explicit arguments or the environment."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TypekitAPIError(TypekitError):
|
|
18
|
+
"""Adobe Fonts rejected a request or returned an invalid response."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
message: str,
|
|
23
|
+
*,
|
|
24
|
+
status_code: int | None = None,
|
|
25
|
+
errors: list[Any] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.errors = errors or []
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: typekit2
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Modern Python client and CLI for the Adobe Fonts (Typekit) API
|
|
5
|
+
Project-URL: Documentation, https://fonts.adobe.com/docs/api
|
|
6
|
+
Project-URL: Repository, https://github.com/fontlaborg/typekit-python
|
|
7
|
+
Author-email: Suchan Lee <lee.suchan@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: adobe-fonts,api,cli,typekit
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: fire>=0.7.1
|
|
21
|
+
Requires-Dist: python-dotenv>=1.2.3
|
|
22
|
+
Requires-Dist: requests>=2.32.5
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
this_file: README.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# typekit2
|
|
30
|
+
|
|
31
|
+
`typekit2` is a modern Python client and Fire CLI for the Adobe Fonts API formerly known as the Typekit API.
|
|
32
|
+
|
|
33
|
+
It replaces the abandoned `typekit` package’s Python 2 code and `setup.py` packaging with Python 3.10+, `pyproject.toml`, HTTPS header authentication, `.env` support, offline tests, and Git-tag-derived versions.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv add typekit2
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For local development:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv sync
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Create `.env` from the supplied example, or export the key directly:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cp .env.example .env
|
|
51
|
+
export TYPEKIT_API_KEY='your-token'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`python-dotenv` loads `.env` without overriding an existing environment variable. Never commit `.env`; it is ignored.
|
|
55
|
+
|
|
56
|
+
## Python API
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from typekit2 import Typekit
|
|
60
|
+
|
|
61
|
+
client = Typekit() # reads TYPEKIT_API_KEY
|
|
62
|
+
|
|
63
|
+
kits = client.list_kits()
|
|
64
|
+
family = client.get_font_family("pcpv")
|
|
65
|
+
variations = client.get_font_variations("pcpv")
|
|
66
|
+
library = client.get_library("full", page=1, per_page=50)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
An explicit key is also supported:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
client = Typekit(api_key="...")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The compatibility keyword `api_token=` is accepted, but new code should use `api_key=` or `TYPEKIT_API_KEY`.
|
|
76
|
+
|
|
77
|
+
### Kit workflow
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
created = client.create_kit(
|
|
81
|
+
"Example",
|
|
82
|
+
["example.com", "www.example.com"],
|
|
83
|
+
[{"id": "pcpv", "subset": "all", "variations": ["n4", "i4"]}],
|
|
84
|
+
)
|
|
85
|
+
kit_id = created["kit"]["id"]
|
|
86
|
+
|
|
87
|
+
client.update_kit(kit_id, name="Example renamed")
|
|
88
|
+
client.add_font(kit_id, "gkmg", variations=["n4", "n7"])
|
|
89
|
+
client.publish_kit(kit_id)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Publishing is asynchronous. Adobe documents that CDN propagation may take several minutes.
|
|
93
|
+
|
|
94
|
+
## CLI
|
|
95
|
+
|
|
96
|
+
The installed `typekit2` command and `python -m typekit2` expose the same Fire CLI. Results are stable JSON.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
typekit2 doctor
|
|
100
|
+
typekit2 kits
|
|
101
|
+
typekit2 kit abc123
|
|
102
|
+
typekit2 kit abc123 --published=true
|
|
103
|
+
typekit2 family pcpv
|
|
104
|
+
typekit2 variations pcpv
|
|
105
|
+
typekit2 libraries
|
|
106
|
+
typekit2 library full --page=1 --per-page=50
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Mutating commands are explicit:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
typekit2 create-kit Example --domains=example.com,www.example.com
|
|
113
|
+
typekit2 add-font abc123 pcpv --variations=n4,i4 --subset=all
|
|
114
|
+
typekit2 update-kit abc123 --name='Renamed kit'
|
|
115
|
+
typekit2 publish-kit abc123
|
|
116
|
+
typekit2 remove-font abc123 pcpv
|
|
117
|
+
typekit2 remove-kit abc123
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
For `--families`, pass a JSON list:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
typekit2 create-kit Example \
|
|
124
|
+
--domains=example.com \
|
|
125
|
+
--families='[{"id":"pcpv","subset":"all","variations":["n4","i4"]}]'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Run `typekit2 --help` or `python -m typekit2 --help` for generated Fire help.
|
|
129
|
+
|
|
130
|
+
## API behavior
|
|
131
|
+
|
|
132
|
+
- Requests use `https://typekit.com/api/v1/json`.
|
|
133
|
+
- Authentication uses the documented `X-Typekit-Token` header; keys never enter URLs or CLI output.
|
|
134
|
+
- Kit writes use URL-encoded Rails-style nested parameters.
|
|
135
|
+
- A 30-second timeout is applied by default and can be changed with `Typekit(timeout=...)`.
|
|
136
|
+
- HTTP, JSON, and documented API errors raise `TypekitAPIError`.
|
|
137
|
+
- `update_kit` sends only supplied fields; omitted fields are not replaced accidentally.
|
|
138
|
+
|
|
139
|
+
See [docs/API.md](docs/API.md) for the method-to-endpoint map and links to Adobe’s authoritative documentation.
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
./test.sh
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The suite is offline: it uses request doubles and never creates, publishes, or deletes a real kit.
|
|
148
|
+
|
|
149
|
+
## Releases
|
|
150
|
+
|
|
151
|
+
Versions come from Git tags through `hatch-vcs`; generated `typekit2/__version__.py` is explicitly ignored. To validate, commit/tag/push the next semantic version, build fresh distributions, and publish with `uv`:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
./publish.sh
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Set `PUBLISH_SKIP_UPLOAD=1` to exercise the Git release and artifact verification flow without uploading to PyPI. `UV_PUBLISH_TOKEN` supplies a PyPI token when Trusted Publishing is unavailable.
|
|
158
|
+
|
|
159
|
+
## License and provenance
|
|
160
|
+
|
|
161
|
+
MIT. The project began as `typekit-python` by Suchan Lee; `typekit2` is its Python 3 modernization.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
typekit2/__init__.py,sha256=pJjFbgyNBw5vE-83-36wJQPdqMWpSYJPNYalbHbDHgU,349
|
|
2
|
+
typekit2/__main__.py,sha256=q4VYnA9fEmmaYdZ9RZ9ZLfW0BkPwwW45f_HV4pvl7J4,4904
|
|
3
|
+
typekit2/__version__.py,sha256=i42Ptxf7aHcFJy2doJ2pAQfHslrfyjzIcTX5tyWcfaE,118
|
|
4
|
+
typekit2/client.py,sha256=od7tiryCLyhBDISVYbvOEOdKq1UTfOoluB2Tm_dNMHM,9934
|
|
5
|
+
typekit2/exceptions.py,sha256=iZ7vu7fPMrkD6jzLgChKFPIsLryvmzUm7zlV_Ud0i30,731
|
|
6
|
+
typekit2-1.0.0.dist-info/METADATA,sha256=eJ3FysEWM89r0upfhj6OVqDuU_Hpcg9e5b9yDt0dG1I,4776
|
|
7
|
+
typekit2-1.0.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
8
|
+
typekit2-1.0.0.dist-info/entry_points.txt,sha256=qKLBuYUeWOtjEZrnpzWCGf4zrIvewpclMF_Bo9nrtR0,52
|
|
9
|
+
typekit2-1.0.0.dist-info/licenses/LICENSE,sha256=k5N3-GTdfsQxacdzsWbI7KNg2rhUyX77tELdYpwdrOI,1076
|
|
10
|
+
typekit2-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Suchan Lee
|
|
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.
|