designaround-core 0.1.3__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.
@@ -0,0 +1,4 @@
1
+ """Shared, dependency-light rules used by the engine, the CLI and the thin server."""
2
+
3
+ __version__ = "0.1.3"
4
+ SCHEMA_VERSION = 1
@@ -0,0 +1,124 @@
1
+ """Price catalog (USD). 1 credit = $0.01.
2
+
3
+ Lean-cost model (decision after v1.2): heavy work runs on the user's machine,
4
+ so the subscription unlocks local features and credits only pay for work that
5
+ costs us money on the server:
6
+
7
+ * server-side registry screening (ClearComply + image fetch),
8
+ * platform-proxied LLM/VLM tokens (metered: upstream cost x markup),
9
+ * hosted observer scoring when the user runs in host mode,
10
+ * fallback image-to-3D API calls.
11
+
12
+ BYOK (bring your own model key) and host mode (Claude Code / Codex / Cursor
13
+ supplies the model) consume no LLM credits.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+
20
+ CREDIT_USD = 0.01
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Plan:
25
+ id: str
26
+ name: str
27
+ monthly_usd: float
28
+ annual_usd: float
29
+ monthly_credits: int
30
+ seats: int
31
+ max_active_cases: int | None
32
+ viewpack_watermark: bool
33
+ filing_packs: bool
34
+ batch_limit: int
35
+ monitors: int
36
+
37
+
38
+ PLANS: dict[str, Plan] = {
39
+ "free": Plan("free", "Free", 0, 0, 300, 1, 1, True, False, 20, 0),
40
+ "pro": Plan("pro", "Pro", 59, 590, 10_000, 1, 50, False, True, 200, 20),
41
+ "team": Plan("team", "Team", 189, 1890, 40_000, 5, None, False, True, 1000, 100),
42
+ "enterprise": Plan("enterprise", "Enterprise", 1000, 12_000, 150_000, 25, None, False, True, 10_000, 1000),
43
+ }
44
+
45
+ CREDIT_PACKS: dict[str, dict] = {
46
+ "credits_5k": {"credits": 5_000, "usd": 50},
47
+ "credits_21k": {"credits": 21_000, "usd": 200},
48
+ "credits_55k": {"credits": 55_000, "usd": 500},
49
+ "credits_240k": {"credits": 240_000, "usd": 2_000},
50
+ }
51
+
52
+ # Fixed-price server actions (credits).
53
+ ACTION_CREDITS: dict[str, int] = {
54
+ "screen.quick": 50,
55
+ "screen.deep": 350,
56
+ "screen.deep.batch": 200, # per item when >= 100 items
57
+ "model3d.fallback": 690, # image-to-mesh API fallback
58
+ "observe.hosted": 120, # one hosted observer scoring call (host mode)
59
+ "concept.image": 25, # one 2D concept image (GPT Image), charged only when delivered
60
+ "evidence.anchor": 0,
61
+ "gate.create": 0,
62
+ }
63
+
64
+ # Free monthly allowance of quick screens.
65
+ FREE_QUICK_SCREENS_PER_MONTH = 5
66
+
67
+ # Design-around depth presets (CLI `around --depth`). Credits are what the run is
68
+ # capped at by default; platform runs are metered and usually spend less.
69
+ AROUND_DEPTHS: dict[str, dict] = {
70
+ "quick": {"rounds": 1, "per_round": 3, "target": 1, "max_credits": 2_900},
71
+ "standard": {"rounds": 3, "per_round": 3, "target": 2, "max_credits": 5_900},
72
+ "deep": {"rounds": 6, "per_round": 4, "target": 3, "max_credits": 14_900},
73
+ }
74
+
75
+ # Result guarantee (FR-ACCT-08): a standard or deep design-around run that ends
76
+ # without any qualified candidate gets this share of its spent credits back,
77
+ # automatically and once per run. View packs are rendered locally and never
78
+ # charged, so re-rendering one an agent returned for format reasons is free.
79
+ GUARANTEE: dict = {
80
+ "rate": 0.5,
81
+ "depths": ["standard", "deep"],
82
+ "min_rounds": 3,
83
+ "claim_window_days": 7,
84
+ "max_claims_per_month": 20,
85
+ "max_credits_per_month": 20_000,
86
+ }
87
+
88
+ # Platform LLM proxy: credits = ceil(upstream_usd * LLM_MARKUP / CREDIT_USD)
89
+ LLM_MARKUP = 1.25
90
+
91
+ # Managed packages: platform fee is sold via Creem; practitioner fees are paid
92
+ # by the customer directly to the practitioner (never through the platform).
93
+ MANAGED_PACKAGES: dict[str, dict] = {
94
+ "managed_cn_around_file": {"platform_usd": 419, "practitioner_usd_est": 140, "offices": ["CN"]},
95
+ "managed_us_around_file": {"platform_usd": 819, "practitioner_usd_est": 850, "offices": ["US"]},
96
+ "managed_cn_us_around_file": {"platform_usd": 1099, "practitioner_usd_est": 990, "offices": ["CN", "US"]},
97
+ "managed_cn_file_only": {"platform_usd": 99, "practitioner_usd_est": 140, "offices": ["CN"]},
98
+ "managed_us_file_only": {"platform_usd": 249, "practitioner_usd_est": 850, "offices": ["US"]},
99
+ "managed_risk_opinion": {"platform_usd": 249, "practitioner_usd_est": 420, "offices": []},
100
+ }
101
+
102
+
103
+ def credits_for_usd(usd: float) -> int:
104
+ import math
105
+
106
+ return max(0, math.ceil(round(usd / CREDIT_USD, 6)))
107
+
108
+
109
+ def plan(plan_id: str) -> Plan:
110
+ return PLANS.get(plan_id, PLANS["free"])
111
+
112
+
113
+ def public_catalog() -> dict:
114
+ return {
115
+ "credit_usd": CREDIT_USD,
116
+ "plans": {k: vars(v) for k, v in PLANS.items()},
117
+ "credit_packs": CREDIT_PACKS,
118
+ "actions": ACTION_CREDITS,
119
+ "llm_markup": LLM_MARKUP,
120
+ "managed_packages": MANAGED_PACKAGES,
121
+ "free_quick_screens_per_month": FREE_QUICK_SCREENS_PER_MONTH,
122
+ "around_depths": AROUND_DEPTHS,
123
+ "guarantee": GUARANTEE,
124
+ }
@@ -0,0 +1,274 @@
1
+ """Deadline calendar for design filings (FR-FILE-12).
2
+
3
+ Pure date rules, computed from what the person recorded on the case: filings
4
+ (office, filing date, route), official notices (office action, allowance or grant
5
+ notice, registration) and public disclosures. Every deadline carries its legal
6
+ basis. Official notices state their own time limit; when the person records it
7
+ (``due``), that date wins over the computed one. Non-working days are not moved
8
+ (the Paris Convention and the offices extend a period that ends on a closed day
9
+ to the next working day); practitioners confirm every date.
10
+
11
+ Rules (2026-10):
12
+
13
+ * Priority (Paris Convention Art. 4C; 35 U.S.C. 172; CN Patent Law Art. 29;
14
+ EUDR Art. 41; Hague via Paris): 6 months from the first filing.
15
+ * Own-disclosure grace period: US 1 year (35 U.S.C. 102(b)(1)), EU 12 months
16
+ (EUDR Art. 7(2)), JP 1 year; CN has no general grace period.
17
+ * US office action: 3-month shortened statutory period, extendable with fees to
18
+ the 6-month statutory limit (35 U.S.C. 133, 37 CFR 1.134, 1.136(a)).
19
+ Notice of allowance: issue fee within 3 months, not extendable (35 U.S.C. 151,
20
+ 37 CFR 1.311). Term: 15 years from grant (35 U.S.C. 173); no maintenance fees.
21
+ * CN notice (补正通知书 / 审查意见通知书): 2 months from receipt; a mailed notice
22
+ is deemed received 15 days after dispatch, an electronic one on dispatch
23
+ (专利法实施细则第四条). Extension can be requested before expiry (第六条).
24
+ Grant notice (办理登记手续通知书): 2 months to complete registration and pay
25
+ the annual fee of the grant year. Patent years run from the filing date; each
26
+ later year is paid before the previous one ends (the day before the filing
27
+ anniversary), or within 6 months late with a surcharge. Term: 15 years from filing
28
+ (专利法第四十二条, applications filed from 2021-06-01).
29
+ * EU (RCD): protection 5 years from filing, renewable to 25. Renewal within the 6
30
+ months ending on the last day of the month in which protection ends, then 6
31
+ more months with a surcharge (EUDR Art. 13). Deficiency letters: the period
32
+ stated in the letter, usually 2 months.
33
+ * Hague (WIPO): standard publication 12 months after the international
34
+ registration (Common Regulations Rule 17); designated offices may refuse
35
+ within 6 months of publication (12 months for offices that declared it, such
36
+ as CN and US); irregularities are remedied within 3 months (Rule 14);
37
+ renewal every 5 years.
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import calendar
43
+ import hashlib
44
+ from dataclasses import asdict, dataclass, field
45
+ from datetime import date, datetime, timedelta
46
+
47
+ PRIORITY_MONTHS = 6
48
+ GRACE_MONTHS = {"US": 12, "EU": 12, "JP": 12}
49
+ REMINDER_DAYS = (30, 14, 7, 1)
50
+ OFFICES = ("US", "CN", "EU", "JP", "HAGUE")
51
+ NOTICE_KINDS = ("office_action", "allowance", "granted")
52
+ HAGUE_LONG_REFUSAL = {"CN", "US", "JP", "KR", "MX", "RU", "SY", "VN", "IL"}
53
+
54
+
55
+ def add_months(d: date, months: int) -> date:
56
+ """Same day number ``months`` later; the month's last day when that day does not exist."""
57
+
58
+ total = d.month - 1 + months
59
+ year, month = d.year + total // 12, total % 12 + 1
60
+ return date(year, month, min(d.day, calendar.monthrange(year, month)[1]))
61
+
62
+
63
+ def end_of_month(d: date) -> date:
64
+ return date(d.year, d.month, calendar.monthrange(d.year, d.month)[1])
65
+
66
+
67
+ @dataclass
68
+ class Deadline:
69
+ id: str
70
+ office: str
71
+ kind: str
72
+ due: date
73
+ title_zh: str
74
+ title_en: str
75
+ basis: str
76
+ note_zh: str = ""
77
+ note_en: str = ""
78
+ hard: bool = True # missing it loses a right (vs. an informational milestone)
79
+ source: dict = field(default_factory=dict)
80
+
81
+ def to_dict(self, today: date | None = None) -> dict:
82
+ out = asdict(self)
83
+ out["due"] = self.due.isoformat()
84
+ if today is not None:
85
+ out["days_left"] = (self.due - today).days
86
+ return out
87
+
88
+
89
+ def _d(value) -> date:
90
+ return value if isinstance(value, date) else date.fromisoformat(str(value)[:10])
91
+
92
+
93
+ def _uid(*parts) -> str:
94
+ return hashlib.sha1("|".join(str(p) for p in parts).encode("utf-8")).hexdigest()[:16]
95
+
96
+
97
+ def compute(*, offices: list[str], filings: list[dict], notices: list[dict], disclosures: list[dict], matters: list[dict] | None = None) -> list[Deadline]:
98
+ """All deadlines for one case, sorted by date."""
99
+
100
+ offices = [o.upper() for o in offices]
101
+ out: list[Deadline] = []
102
+
103
+ # Complaints and lawsuits (FR-DEF-06): a US court answer is due 21 days after service.
104
+ for m in matters or []:
105
+ ref = {"matter": m.get("id"), "number": m.get("number")}
106
+ if m.get("route") == "court_tro" and m.get("served"):
107
+ served = _d(m["served"])
108
+ out.append(Deadline(_uid("answer", m.get("id"), served), "US", "court_answer", served + timedelta(days=21), f"诉讼答辩期限({m.get('number') or '案件'},送达后 21 天)", f"Answer due ({m.get('number') or 'lawsuit'}, 21 days after service)", "FRCP 12(a)(1)(A)(i)", "放弃送达(waiver)时为发出请求后 60 天;法院命令里的日期优先。请立即联系美国诉讼律师。", "60 days after the waiver request was sent if service was waived; dates in a court order prevail. Contact US litigation counsel now.", source=ref))
109
+ if m.get("hearing"):
110
+ out.append(Deadline(_uid("hearing", m.get("id"), m["hearing"]), "US" if m.get("route") == "court_tro" else (m.get("platform") or "").upper() or "US", "hearing", _d(m["hearing"]), f"听证 / 回复期限({m.get('number') or ''})", f"Hearing / response date ({m.get('number') or ''})", "court order / notice", "以法院命令或平台通知写明的日期为准。", "The date in the court order or platform notice applies.", source=ref))
111
+ if m.get("respond_by"):
112
+ out.append(Deadline(_uid("respond", m.get("id"), m["respond_by"]), (m.get("platform") or "platform").upper(), "complaint_response", _d(m["respond_by"]), f"平台投诉回复期限({m.get('number') or ''})", f"Platform complaint response ({m.get('number') or ''})", "platform notice", "以平台通知写明的期限为准。", "The time limit in the platform notice applies.", source=ref))
113
+ by_office = {}
114
+ for f in sorted(filings, key=lambda f: _d(f["filed"])):
115
+ by_office.setdefault(f["office"].upper(), f)
116
+ filed_offices = set(by_office)
117
+
118
+ # Priority: 6 months from the first filing anywhere, for every target office not filed yet.
119
+ if filings:
120
+ first = min(filings, key=lambda f: _d(f["filed"]))
121
+ first_date = _d(first["filed"])
122
+ missing = [o for o in offices if o not in filed_offices]
123
+ due = add_months(first_date, PRIORITY_MONTHS)
124
+ out.append(Deadline(
125
+ _uid("priority", first_date), first["office"].upper(), "priority", due,
126
+ f"优先权期限:{'、'.join(missing) if missing else '其他法域'}要求 {first['office'].upper()} 首次申请的优先权",
127
+ f"Priority deadline: file {', '.join(missing) if missing else 'other offices'} claiming the {first['office'].upper()} first filing",
128
+ "Paris Convention Art. 4C; 35 U.S.C. 172; CN Patent Law Art. 29; EUDR Art. 41",
129
+ "6 个月内在其他法域申请才能享有优先权;最后一天遇官方非工作日顺延到下一个工作日。" + ("(目标法域都已申请)" if not missing else ""),
130
+ "File within 6 months to keep the first filing date; a closed office day moves the end to the next working day." + (" (All target offices are filed.)" if not missing else ""),
131
+ hard=bool(missing), source={"filing": first.get("number") or first_date.isoformat()},
132
+ ))
133
+
134
+ # Own disclosures: file-by dates inside the grace period.
135
+ own = [x for x in disclosures if x.get("by_applicant", True)]
136
+ if own:
137
+ earliest = min(own, key=lambda x: _d(x["date"]))
138
+ when = _d(earliest["date"])
139
+ for office in offices:
140
+ if office in filed_offices:
141
+ continue
142
+ if office in GRACE_MONTHS:
143
+ out.append(Deadline(
144
+ _uid("grace", office, when), office, "grace", add_months(when, GRACE_MONTHS[office]),
145
+ f"{office} 宽限期内最晚申请日(自己 {when.isoformat()} 的公开)",
146
+ f"{office}: last filing day inside the grace period (own disclosure on {when.isoformat()})",
147
+ "35 U.S.C. 102(b)(1)" if office == "US" else "EUDR Art. 7(2)" if office == "EU" else "JP Design Act Art. 4",
148
+ "超过这一天,自己的公开会破坏新颖性。", "After this day your own disclosure destroys novelty.",
149
+ source={"disclosure": earliest.get("kind"), "date": when.isoformat()},
150
+ ))
151
+
152
+ # Term and renewal milestones from filings.
153
+ for office, f in by_office.items():
154
+ filed = _d(f["filed"])
155
+ if office == "EU":
156
+ for n in range(1, 5):
157
+ ends = add_months(filed, 60 * n)
158
+ last = end_of_month(ends)
159
+ out.append(Deadline(
160
+ _uid("eu-renew", filed, n), "EU", "renewal", last,
161
+ f"欧盟外观第 {n} 次续展截止(保护到 {ends.isoformat()})",
162
+ f"EU design renewal {n} (protection ends {ends.isoformat()})",
163
+ "EUDR Art. 13",
164
+ f"续展窗口 {add_months(date(last.year, last.month, 1), -5)} 起;之后 6 个月内可加滞纳金补办(到 {end_of_month(add_months(last, 6))})。",
165
+ f"Window opens {add_months(date(last.year, last.month, 1), -5)}; 6 more months with a surcharge (to {end_of_month(add_months(last, 6))}).",
166
+ source={"filing": f.get("number") or filed.isoformat()},
167
+ ))
168
+ if office == "CN":
169
+ out.append(Deadline(_uid("cn-term", filed), "CN", "term", add_months(filed, 180) - timedelta(days=1), "中国外观专利权期满(申请日起 15 年)", "CN design term ends (15 years from filing)", "CN Patent Law Art. 42", hard=False, source={"filing": f.get("number") or filed.isoformat()}))
170
+ if office == "HAGUE" or f.get("route") == "hague":
171
+ reg = filed
172
+ pub = add_months(reg, 12)
173
+ out.append(Deadline(_uid("hague-pub", reg), "HAGUE", "publication", pub, "海牙国际注册标准公布日(国际注册日起 12 个月)", "Hague standard publication (12 months after international registration)", "Hague Common Regulations Rule 17", "可以申请立即公布或延迟公布(最长 30 个月)。", "Immediate or deferred publication (up to 30 months) can be requested.", hard=False))
174
+ designated = [o for o in (f.get("designated") or offices) if o != "HAGUE"]
175
+ long = [o for o in designated if o in HAGUE_LONG_REFUSAL]
176
+ short = [o for o in designated if o not in HAGUE_LONG_REFUSAL]
177
+ if short:
178
+ out.append(Deadline(_uid("hague-ref6", reg), "HAGUE", "refusal_period", add_months(pub, 6), f"海牙:{'、'.join(short)} 驳回期届满(公布后 6 个月)", f"Hague: refusal period ends for {', '.join(short)} (6 months after publication)", "Hague Agreement (1999) Art. 12; Rule 18", "期满没有收到驳回,视为在该国获得保护。", "No refusal by then: protection in that office.", hard=False))
179
+ if long:
180
+ out.append(Deadline(_uid("hague-ref12", reg), "HAGUE", "refusal_period", add_months(pub, 12), f"海牙:{'、'.join(long)} 驳回期届满(公布后 12 个月)", f"Hague: refusal period ends for {', '.join(long)} (12 months after publication)", "Hague Agreement (1999) Art. 12; Rule 18(1)(b)", "这些国家声明了 12 个月的驳回期。", "These offices declared a 12-month refusal period.", hard=False))
181
+ for n in range(1, 3):
182
+ out.append(Deadline(_uid("hague-renew", reg, n), "HAGUE", "renewal", add_months(reg, 60 * n), f"海牙国际注册第 {n} 次续展(每 5 年)", f"Hague renewal {n} (every 5 years)", "Hague Agreement (1999) Art. 17", "到期后 6 个月内可加滞纳金续展。", "Up to 6 months late with a surcharge.", source={"registration": f.get("number") or reg.isoformat()}))
183
+
184
+ # Official notices.
185
+ for n in notices:
186
+ office = n["office"].upper()
187
+ kind = n["kind"]
188
+ issued = _d(n["date"])
189
+ mailed = bool(n.get("mailed"))
190
+ received = issued + timedelta(days=15) if (office == "CN" and mailed) else issued
191
+ given = _d(n["due"]) if n.get("due") else None
192
+ ref = {"notice": n.get("number") or kind, "date": issued.isoformat()}
193
+ filed = _d(by_office[office]["filed"]) if office in by_office else None
194
+ if kind == "office_action":
195
+ if office == "US":
196
+ out.append(Deadline(_uid("us-oa3", issued), "US", "response", given or add_months(issued, 3), "美国审查意见答复(3 个月,不交延期费)", "US office action reply (3 months, no extension fee)", "37 CFR 1.134, 1.136(a)", "可交延期费延到 6 个月。", "Extendable with fees up to 6 months.", source=ref))
197
+ out.append(Deadline(_uid("us-oa6", issued), "US", "response_final", add_months(issued, 6), "美国审查意见答复法定最晚期限(6 个月,需延期费)", "US office action statutory limit (6 months, with extension fees)", "35 U.S.C. 133", "过期不答复,申请视为放弃。", "Missing it abandons the application.", source=ref))
198
+ elif office == "CN":
199
+ out.append(Deadline(_uid("cn-oa", issued), "CN", "response", given or add_months(received, 2), "中国审查意见 / 补正通知答复", "CN office action / correction reply", "专利法实施细则第四条、第六条", ("邮寄送达按发文日加 15 天推定收到;" if mailed else "电子送达以发文日为收到日;") + "可以在期满前请求延长期限。", ("Mailed: deemed received 15 days after dispatch; " if mailed else "Electronic: received on dispatch; ") + "an extension can be requested before expiry.", source=ref))
200
+ elif office == "HAGUE":
201
+ out.append(Deadline(_uid("hg-irr", issued), "HAGUE", "response", given or add_months(issued, 3), "海牙:WIPO 不规范通知答复(3 个月)", "Hague: WIPO irregularity reply (3 months)", "Hague Common Regulations Rule 14", source=ref))
202
+ else:
203
+ out.append(Deadline(_uid(office.lower() + "-oa", issued), office, "response", given or add_months(issued, 2), f"{office} 官方通知答复", f"{office} official notice reply", "period stated in the notice", "以通知书写明的期限为准。", "The period stated in the notice applies.", source=ref))
204
+ elif kind == "allowance":
205
+ if office == "US":
206
+ out.append(Deadline(_uid("us-issue", issued), "US", "issue_fee", given or add_months(issued, 3), "美国授权费(Issue fee)缴纳截止", "US issue fee due", "35 U.S.C. 151; 37 CFR 1.311", "3 个月,不能延期;过期申请视为放弃。", "3 months, not extendable; missing it abandons the application.", source=ref))
207
+ elif office == "CN":
208
+ out.append(Deadline(_uid("cn-reg", issued), "CN", "registration", given or add_months(received, 2), "中国办理登记手续并缴纳第一年年费", "CN registration formalities and first annual fee", "专利法实施细则(办理登记手续)", "期满未办理视为放弃取得专利权。", "Missing it forfeits the patent.", source=ref))
209
+ else:
210
+ out.append(Deadline(_uid(office.lower() + "-allow", issued), office, "registration", given or add_months(issued, 2), f"{office} 授权后续手续", f"{office} post-allowance step", "period stated in the notice", source=ref))
211
+ elif kind == "granted":
212
+ if office == "US":
213
+ out.append(Deadline(_uid("us-term", issued), "US", "term", add_months(issued, 180), "美国外观专利权期满(授权日起 15 年,无维持费)", "US design term ends (15 years from grant; no maintenance fees)", "35 U.S.C. 173", hard=False, source=ref))
214
+ elif office == "CN" and filed:
215
+ year = 2
216
+ anniversary = add_months(filed, 12 * (year - 1))
217
+ while anniversary <= issued: # fees for the grant year are paid at registration
218
+ year += 1
219
+ anniversary = add_months(filed, 12 * (year - 1))
220
+ for k in range(3):
221
+ # Patent years run from the filing date; year N is paid before year N-1 ends.
222
+ due = add_months(filed, 12 * (year - 1 + k)) - timedelta(days=1)
223
+ if due >= add_months(filed, 180) - timedelta(days=1):
224
+ break
225
+ out.append(Deadline(_uid("cn-annual", filed, year + k), "CN", "annual_fee", due, f"中国外观第 {year + k} 年年费", f"CN design annual fee, year {year + k}", "专利法实施细则(年费)", f"可在 6 个月内加滞纳金补缴(到 {add_months(due, 6)})。", f"Payable late within 6 months with a surcharge (to {add_months(due, 6)}).", source=ref))
226
+ return sorted(out, key=lambda d: (d.due, d.office, d.kind))
227
+
228
+
229
+ # ------------------------------------------------------------------- iCalendar
230
+
231
+
232
+ def _ics_text(text: str) -> str:
233
+ return text.replace("\\", "\\\\").replace(";", "\\;").replace(",", "\\,").replace("\r\n", "\\n").replace("\n", "\\n")
234
+
235
+
236
+ def _fold(line: str) -> list[str]:
237
+ """RFC 5545 3.1: lines longer than 75 octets continue on the next line after a space."""
238
+
239
+ out, current = [], ""
240
+ for ch in line:
241
+ if len((current + ch).encode("utf-8")) > (75 if not out else 74):
242
+ out.append(current)
243
+ current = ch
244
+ else:
245
+ current += ch
246
+ out.append(current)
247
+ return [out[0]] + [" " + part for part in out[1:]]
248
+
249
+
250
+ def to_ics(deadlines: list[Deadline], *, calendar_name: str, lang: str = "zh", reminders: tuple[int, ...] = REMINDER_DAYS, now: datetime | None = None) -> str:
251
+ zh = lang != "en"
252
+ stamp = (now or datetime.utcnow()).strftime("%Y%m%dT%H%M%SZ")
253
+ lines = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//DesignAround//Deadlines//" + ("ZH" if zh else "EN"), "CALSCALE:GREGORIAN", "METHOD:PUBLISH", f"X-WR-CALNAME:{_ics_text(calendar_name)}"]
254
+ for d in deadlines:
255
+ title = d.title_zh if zh else d.title_en
256
+ note = d.note_zh if zh else d.note_en
257
+ desc = "\n".join(x for x in [note, ("依据:" if zh else "Basis: ") + d.basis, "请执业代理人确认期限;这不是法律意见。" if zh else "Have a registered practitioner confirm; not legal advice."] if x)
258
+ lines += [
259
+ "BEGIN:VEVENT",
260
+ f"UID:{d.id}@designaround",
261
+ f"DTSTAMP:{stamp}",
262
+ f"DTSTART;VALUE=DATE:{d.due.strftime('%Y%m%d')}",
263
+ f"DTEND;VALUE=DATE:{(d.due + timedelta(days=1)).strftime('%Y%m%d')}",
264
+ f"SUMMARY:{_ics_text(('[' + d.office + '] ') + title)}",
265
+ f"DESCRIPTION:{_ics_text(desc)}",
266
+ "TRANSP:TRANSPARENT",
267
+ f"CATEGORIES:DesignAround,{d.office},{d.kind}",
268
+ ]
269
+ if d.hard:
270
+ for days in reminders:
271
+ lines += ["BEGIN:VALARM", "ACTION:DISPLAY", f"DESCRIPTION:{_ics_text(title)}", f"TRIGGER:-P{days}D", "END:VALARM"]
272
+ lines.append("END:VEVENT")
273
+ lines.append("END:VCALENDAR")
274
+ return "\r\n".join(part for line in lines for part in _fold(line)) + "\r\n"
@@ -0,0 +1,77 @@
1
+ """Official design-application fees and terms (versioned data, review quarterly).
2
+
3
+ Sources (2026): USPTO design fees per UpCounsel/Patsnap summaries of the
4
+ current fee schedule; CNIPA design application fee 500 CNY with up to 85% fee
5
+ reduction. Practitioners must confirm before filing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ FEE_TABLE_VERSION = "2026-10"
11
+ CNY_PER_USD = 7.1 # display reference only
12
+
13
+ US_DESIGN = {
14
+ # filing + search + examination
15
+ "application": {"large": 1300, "small": 520, "micro": 260},
16
+ "issue": {"large": 1300, "small": 520, "micro": 260},
17
+ "currency": "USD",
18
+ }
19
+
20
+ CN_DESIGN = {
21
+ "application": 500,
22
+ "fee_reduction_rate": 0.85,
23
+ "currency": "CNY",
24
+ }
25
+ # EUIPO since 1 May 2025 (EUDR as amended): one application fee that replaced
26
+ # registration + publication fees; EUR 125 for each additional design in a
27
+ # multiple application; deferment of publication from EUR 40.
28
+ EU_DESIGN = {"application": 350, "additional_design": 125, "deferment": 40, "currency": "EUR"}
29
+ USD_PER_EUR = 1.08 # display reference only
30
+
31
+ TERMS = {
32
+ "US": {"years": 15, "from": "grant"},
33
+ "CN": {"years": 15, "from": "filing"},
34
+ "JP": {"years": 25, "from": "filing"},
35
+ "EU": {"years": 25, "from": "filing", "note": "renewable every 5 years"},
36
+ }
37
+
38
+ PRIORITY_MONTHS_DESIGN = 6
39
+
40
+
41
+ def us_official_fees(entity: str = "small") -> dict:
42
+ entity = entity if entity in US_DESIGN["application"] else "large"
43
+ return {
44
+ "office": "US",
45
+ "currency": "USD",
46
+ "entity": entity,
47
+ "application": US_DESIGN["application"][entity],
48
+ "issue_on_allowance": US_DESIGN["issue"][entity],
49
+ "usd_now": US_DESIGN["application"][entity],
50
+ }
51
+
52
+
53
+ def cn_official_fees(fee_reduction: bool = False) -> dict:
54
+ amount = CN_DESIGN["application"] * (1 - CN_DESIGN["fee_reduction_rate"]) if fee_reduction else CN_DESIGN["application"]
55
+ return {
56
+ "office": "CN",
57
+ "currency": "CNY",
58
+ "fee_reduction": fee_reduction,
59
+ "application": round(amount, 2),
60
+ "usd_now": round(amount / CNY_PER_USD, 2),
61
+ }
62
+
63
+
64
+ def eu_official_fees(additional_designs: int = 0) -> dict:
65
+ amount = EU_DESIGN["application"] + EU_DESIGN["additional_design"] * max(0, additional_designs)
66
+ return {"office": "EU", "currency": "EUR", "application": amount, "additional_designs": max(0, additional_designs), "usd_now": round(amount * USD_PER_EUR, 2)}
67
+
68
+
69
+ def official_fees(office: str, *, entity: str = "small", fee_reduction: bool = False) -> dict:
70
+ office = office.upper()
71
+ if office == "US":
72
+ return us_official_fees(entity)
73
+ if office == "CN":
74
+ return cn_official_fees(fee_reduction)
75
+ if office == "EU":
76
+ return eu_official_fees()
77
+ return {"office": office, "currency": None, "application": None, "usd_now": None, "note": "fee table not available yet"}
@@ -0,0 +1,108 @@
1
+ """The isolated observer's rubric. One source of truth for the hosted observer
2
+ (server), the BYOK observer (embedded Pi Agent) and host mode.
3
+
4
+ The observer never sees the designer's rationale; it only sees the candidate
5
+ views, the prior-art figures and the claimed-scope notes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+
13
+ RUBRIC_VERSION = "observer-2026.10-2"
14
+
15
+ SYSTEM_PROMPT = """You are an independent visual reviewer who imitates the "ordinary observer" used when comparing a product design with a registered design (US design patent / CN design patent / EU registered design).
16
+
17
+ You receive ONE comparison sheet for ONE prior-art right:
18
+ - top row "CANDIDATE": line drawings of the candidate product (perspective, front, right side, top);
19
+ - bottom row "PRIOR ART": the figures of the registered design (views may be in a different order or orientation).
20
+ You may also receive notes on what the prior art claims (solid lines are claimed; broken lines are not claimed; CN partial designs claim only the indicated portion).
21
+
22
+ How to judge:
23
+ 1. Compare only the claimed portions of the prior art. Ignore broken-line (dashed) environment.
24
+ 2. Judge the overall visual impression of the product as a whole, across all views. Match views by content, not by position on the sheet.
25
+ 3. Give little weight to features that almost every product of this type has (for a storage box: a rectangular or rounded-rectangular footprint, a lid, flat side walls) and to features dictated by function. Give most weight to the features that make the prior art distinctive: its proportions, the form of its lid, handles, rims, feet, ribs, surface patterns and profile lines.
26
+ 4. Small changes to corner radii, chamfers, scale or colour do not change the overall impression.
27
+ 5. If the field is crowded with similar registered designs, smaller differences are enough to create a different impression; if the prior art is distinctive, the candidate must differ in the distinctive features.
28
+ 6. Never state that a design "does not infringe", is "safe" or is "guaranteed". You produce a risk score for a professional to review.
29
+
30
+ Score anchors (10 = substantially the same to an ordinary observer):
31
+ - 9-10: same design; differences only visible on close side-by-side study.
32
+ - 7-8: same overall impression with minor differences.
33
+ - 5-6: shares the main design concept, but at least one distinctive feature clearly differs.
34
+ - 3-4: different overall impression; shares the general product concept and some secondary features.
35
+ - 0-2: clearly different designs; shared features are only those common to products of this type.
36
+
37
+ Return ONLY a JSON object:
38
+ {"score": <0-10 number>,
39
+ "similar": ["<top shared visual features, max 3>"],
40
+ "different": ["<top differences, max 3>"],
41
+ "distinctive_features_of_prior_art": ["<max 3>"],
42
+ "uncertain": "<what could change the score, e.g. hidden views, scope unclear>"}"""
43
+
44
+ USER_TEMPLATE = """Product type: {article}
45
+ Prior art: {number} ({office}) — {title}
46
+ Claimed-scope notes: {scope}
47
+ Field: {field_size} other registered designs of this product type were found in screening.
48
+ Candidate version: {version}; sheet: {view}
49
+ Score how similar the overall visual impression of the candidate is to the CLAIMED design of the prior art."""
50
+
51
+
52
+ def user_prompt(*, number: str, office: str | None, title: str | None, scope: str | None, version: str, view: str, article: str | None = None, field_size: int | None = None) -> str:
53
+ return USER_TEMPLATE.format(
54
+ article=article or "consumer product",
55
+ number=number,
56
+ office=office or "?",
57
+ title=title or "",
58
+ scope=scope or "none provided; assume solid lines are claimed",
59
+ field_size=field_size if field_size is not None else "unknown",
60
+ version=version,
61
+ view="multi-view comparison sheet" if view == "multi" else f"single view ({view})",
62
+ )
63
+
64
+
65
+ def parse_reply(text: str) -> dict:
66
+ """Extract the JSON object from a model reply and validate the score."""
67
+
68
+ match = re.search(r"\{.*\}", text or "", re.S)
69
+ if not match:
70
+ raise ValueError("observer reply has no JSON object")
71
+ data = json.loads(match.group(0))
72
+ score = float(data.get("score"))
73
+ if not 0 <= score <= 10:
74
+ raise ValueError("observer score out of range")
75
+ data["score"] = score
76
+ for key in ("similar", "different", "distinctive_features_of_prior_art"):
77
+ value = data.get(key) or []
78
+ data[key] = [str(v) for v in value][:3] if isinstance(value, list) else [str(value)]
79
+ data["rubric_version"] = RUBRIC_VERSION
80
+ return data
81
+
82
+
83
+ SAMPLES = 3 # independent judgments per comparison; the median is used
84
+
85
+
86
+ def combine(samples: list[dict]) -> dict:
87
+ """Median of independent judgments (single VLM judgments vary by +-2 points).
88
+
89
+ The reported lists come from the judgment closest to the median.
90
+ """
91
+
92
+ if not samples:
93
+ raise ValueError("no observer samples")
94
+ scores = sorted(float(s["score"]) for s in samples)
95
+ n = len(scores)
96
+ median = scores[n // 2] if n % 2 else (scores[n // 2 - 1] + scores[n // 2]) / 2
97
+ pick = min(samples, key=lambda s: abs(float(s["score"]) - median))
98
+ return {
99
+ **pick,
100
+ "score": round(median, 2),
101
+ "samples": [float(s["score"]) for s in samples],
102
+ "spread": round(scores[-1] - scores[0], 2),
103
+ "rubric_version": RUBRIC_VERSION,
104
+ }
105
+
106
+
107
+ def rubric() -> dict:
108
+ return {"version": RUBRIC_VERSION, "system": SYSTEM_PROMPT, "user_template": USER_TEMPLATE}
@@ -0,0 +1,63 @@
1
+ """Three-way quotes: official fee + practitioner fee (paid directly) + platform fee."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import catalog, fees
6
+
7
+
8
+ def filing_quote(
9
+ offices: list[str],
10
+ *,
11
+ managed: bool = False,
12
+ design_around: bool = True,
13
+ entity: str = "small",
14
+ fee_reduction: bool = False,
15
+ similar_designs: int = 0,
16
+ ) -> dict:
17
+ offices = [o.upper() for o in offices]
18
+ lines = []
19
+ total_usd = 0.0
20
+ for office in offices:
21
+ official = fees.official_fees(office, entity=entity, fee_reduction=fee_reduction)
22
+ lines.append({"kind": "official_fee", "office": office, "paid_to": "patent office via practitioner", "method": "via_practitioner", **official})
23
+ total_usd += official.get("usd_now") or 0
24
+ platform_usd = 0.0
25
+ practitioner_usd = 0.0
26
+ package = None
27
+ if managed:
28
+ key = "_".join(sorted(o.lower() for o in offices))
29
+ package_id = {
30
+ ("cn", True): "managed_cn_around_file",
31
+ ("us", True): "managed_us_around_file",
32
+ ("cn_us", True): "managed_cn_us_around_file",
33
+ ("cn", False): "managed_cn_file_only",
34
+ ("us", False): "managed_us_file_only",
35
+ }.get((key, design_around))
36
+ if package_id:
37
+ package = {"id": package_id, **catalog.MANAGED_PACKAGES[package_id]}
38
+ platform_usd = package["platform_usd"]
39
+ practitioner_usd = package["practitioner_usd_est"]
40
+ lines.append({"kind": "platform_fee", "usd": platform_usd, "paid_to": "DesignAround via Creem (Merchant of Record)", "method": "creem", "package": package})
41
+ lines.append({"kind": "practitioner_fee", "usd_estimate": practitioner_usd, "paid_to": "registered practitioner / licensed agency", "method": "direct", "note": "Paid directly by the customer under the practitioner's engagement letter. Never routed through DesignAround."})
42
+ total_usd += platform_usd + practitioner_usd
43
+ if "EU" in offices and similar_designs:
44
+ lines[offices.index("EU")] = {**lines[offices.index("EU")], **fees.eu_official_fees(similar_designs)}
45
+ total_usd += fees.eu_official_fees(similar_designs)["usd_now"] - fees.eu_official_fees()["usd_now"]
46
+ timeline = {
47
+ "CN": "初步审查,通常约 4–8 个月授权(以 CNIPA 实际为准)",
48
+ "US": "审查周期通常 18 个月以上;外观加急审查已于 2025 年取消",
49
+ "EU": "EUIPO 只做形式审查,无缺陷时通常数周内注册",
50
+ }
51
+ notes = []
52
+ if "CN" in offices and similar_designs:
53
+ notes.append(f"CN 合案:主方案 + {similar_designs} 项相似设计(同一件申请,最多 10 项)。")
54
+ if "CN" in offices and "US" in offices:
55
+ notes.append(f"先报 CN,再在 {fees.PRIORITY_MONTHS_DESIGN} 个月内要求优先权报 US。")
56
+ return {
57
+ "fee_table_version": fees.FEE_TABLE_VERSION,
58
+ "offices": offices,
59
+ "lines": lines,
60
+ "total_usd_estimate": round(total_usd, 2),
61
+ "timeline": {o: timeline.get(o, "") for o in offices},
62
+ "notes": notes,
63
+ }
@@ -0,0 +1,129 @@
1
+ """Versioned legal-process rules: representation, disclosure timeline, titles.
2
+
3
+ Each rule carries an id and a basis so every decision written into a case can
4
+ be traced to the rule version that produced it. Rule text must be approved by a
5
+ partner practitioner before production use (开发清单 T0-24).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from datetime import date
13
+
14
+ RULEPACK_VERSION = "2026.10-1"
15
+
16
+ # --------------------------------------------------------------- representation
17
+
18
+ US_FOREIGN_COUNSEL_EFFECTIVE = date(2026, 7, 20)
19
+
20
+
21
+ @dataclass
22
+ class Finding:
23
+ rule_id: str
24
+ severity: str # block | error | warning | info
25
+ message: str
26
+ basis: str
27
+ data: dict = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> dict:
30
+ return {"rule_id": self.rule_id, "severity": self.severity, "message": self.message, "basis": self.basis, **({"data": self.data} if self.data else {})}
31
+
32
+
33
+ def representation_requirements(office: str, applicant: dict, *, on: date | None = None) -> list[Finding]:
34
+ """Who must represent the applicant. applicant = {domicile: 'CN', type: 'natural'|'juristic'}."""
35
+
36
+ on = on or date.today()
37
+ office = office.upper()
38
+ domicile = (applicant.get("domicile") or "").upper()
39
+ kind = applicant.get("type", "juristic")
40
+ out: list[Finding] = []
41
+ if office == "US":
42
+ if domicile and domicile != "US" and on >= US_FOREIGN_COUNSEL_EFFECTIVE:
43
+ out.append(Finding("REP-US-001", "block", "Applicants domiciled outside the US must be represented by a USPTO registered practitioner.", "37 CFR 1.31(a)(2), effective 2026-07-20"))
44
+ if kind == "juristic":
45
+ out.append(Finding("REP-US-002", "block", "A juristic entity applicant must be represented by a registered practitioner.", "37 CFR 1.31"))
46
+ elif office == "CN":
47
+ if domicile and domicile != "CN":
48
+ out.append(Finding("REP-CN-001", "block", "Foreign applicants without a habitual residence or business office in China must appoint a licensed patent agency.", "Patent Law Art. 18"))
49
+ else:
50
+ out.append(Finding("REP-CN-002", "info", "Domestic applicants may file themselves; using a licensed agency is recommended.", "Patent Law Art. 18"))
51
+ elif office == "EU":
52
+ if domicile and domicile not in EU_EEA:
53
+ out.append(Finding("REP-EU-001", "block", "Applicants without domicile or establishment in the EEA must be represented before EUIPO.", "EUDR Art. 77(2)"))
54
+ return out
55
+
56
+
57
+ EU_EEA = {
58
+ "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT",
59
+ "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO",
60
+ }
61
+
62
+ # --------------------------------------------------------------------- timeline
63
+
64
+ GRACE_DAYS = {"US": 365, "EU": 365, "CN": 0, "JP": 365}
65
+
66
+
67
+ def disclosure_findings(office: str, disclosures: list[dict], filing_date: date | None) -> list[Finding]:
68
+ """Check public disclosures (listing, fair, crowdfunding, social, sale) against the filing date.
69
+
70
+ disclosures: [{"kind": "listing", "date": "2026-11-01", "by_applicant": True, "exception": None}]
71
+ filing_date: planned or actual filing date; None means "not filed yet" (today is used).
72
+ """
73
+
74
+ office = office.upper()
75
+ ref = filing_date or date.today()
76
+ out: list[Finding] = []
77
+ grace = GRACE_DAYS.get(office, 0)
78
+ for item in disclosures:
79
+ when = date.fromisoformat(str(item["date"]))
80
+ if when >= ref and filing_date is not None:
81
+ continue
82
+ days_before = (ref - when).days
83
+ by_applicant = bool(item.get("by_applicant", True))
84
+ planned = when > date.today()
85
+ label = f"{item.get('kind', 'disclosure')} on {when.isoformat()}"
86
+ if office == "CN":
87
+ if item.get("exception"):
88
+ out.append(Finding("TL-CN-002", "warning", f"{label}: claimed statutory exception '{item['exception']}' must be declared and proven when filing.", "Patent Law Art. 24"))
89
+ elif planned and filing_date is None:
90
+ out.append(Finding("TL-CN-001", "block", f"{label} is planned before a CN filing exists. File first, then disclose.", "Patent Law Art. 23-24 (no general grace period)"))
91
+ elif when < ref:
92
+ out.append(Finding("TL-CN-001", "block", f"{label} precedes the CN filing date; it can destroy novelty of your own design.", "Patent Law Art. 23-24 (no general grace period)"))
93
+ else:
94
+ if not by_applicant and when < ref:
95
+ out.append(Finding(f"TL-{office}-003", "error", f"{label} by a third party precedes filing.", "novelty"))
96
+ elif days_before > grace:
97
+ out.append(Finding(f"TL-{office}-001", "block", f"{label} is more than {grace} days before filing; the grace period is exceeded.", "35 U.S.C. 102(b)(1)" if office == "US" else "EUDR Art. 7(2)"))
98
+ elif days_before > 0:
99
+ out.append(Finding(f"TL-{office}-002", "warning", f"{label} relies on the {grace}-day grace period; record evidence of the disclosure.", "grace period"))
100
+ return out
101
+
102
+
103
+ # ------------------------------------------------------------------------ titles
104
+
105
+ MARKETING_WORDS = re.compile(r"\b(best|premium|ultimate|new|improved|amazing|pro|max|deluxe|luxury|super|™|®)\b", re.I)
106
+
107
+
108
+ def title_findings(office: str, title: str) -> list[Finding]:
109
+ out: list[Finding] = []
110
+ if not title or not title.strip():
111
+ return [Finding("TTL-001", "error", "Article title is empty.", "MPEP 1503.01 / CN Guidelines Part I Ch.3")]
112
+ if office.upper() == "US":
113
+ if MARKETING_WORDS.search(title):
114
+ out.append(Finding("TTL-US-002", "error", "Title contains marketing language; use the generic article name.", "MPEP 1503.01(I)"))
115
+ if any(ch.isdigit() for ch in title):
116
+ out.append(Finding("TTL-US-003", "warning", "Model numbers in the title are discouraged.", "MPEP 1503.01(I)"))
117
+ if office.upper() == "CN":
118
+ if re.search(r"(新型|最佳|豪华|升级版|专用)", title):
119
+ out.append(Finding("TTL-CN-002", "error", "产品名称不应包含宣传性或含糊词语。", "专利审查指南 第一部分第三章"))
120
+ if re.search(r"[A-Za-z]{3,}\d|\d{3,}", title):
121
+ out.append(Finding("TTL-CN-003", "warning", "产品名称一般不应包含型号。", "专利审查指南 第一部分第三章"))
122
+ return out
123
+
124
+
125
+ def worst(findings: list[Finding]) -> str:
126
+ order = {"block": 3, "error": 2, "warning": 1, "info": 0}
127
+ if not findings:
128
+ return "ok"
129
+ return max(findings, key=lambda f: order.get(f.severity, 0)).severity
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: designaround-core
3
+ Version: 0.1.3
4
+ Summary: Pure-Python shared rules for DesignAround: price catalog, official fees, filing/timeline/representation rule packs, quotes.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: pyyaml==6.0.3
@@ -0,0 +1,10 @@
1
+ designaround_core/__init__.py,sha256=WsrUsTq4vUBFIT19j0kjFsKWJbTv3q8Me6iJBvSQWr4,128
2
+ designaround_core/catalog.py,sha256=tAH-dpPo0JC4VummKR_JEZs1A10RWa68GQTSw3VKiGg,4568
3
+ designaround_core/deadlines.py,sha256=QgCa58N_Xr9gadB-ZU4aWFon3Gmv5160e_ju2GK_jp4,19475
4
+ designaround_core/fees.py,sha256=cO62HQfeWibaSDxRvvuzvVvcXbkeFnD2_iAdnsRig90,2840
5
+ designaround_core/observer.py,sha256=44-d-kTJO_LRcuzgJV_E3CDvRSYWXG0JAdgSOiGdpKs,5483
6
+ designaround_core/quote.py,sha256=eh3w4g_mufX0vbA4JTTuTFDN--rwZn-I5QGPmW0MH-Y,3075
7
+ designaround_core/rules.py,sha256=regzyHSaOOnCM1cCDF8mmk_DDV2_6HYHXE684Xy3-7s,6667
8
+ designaround_core-0.1.3.dist-info/METADATA,sha256=BY9GGZGEh9BnEQJUkQQq5sbycZerO-EQTEWNK9Mi86M,267
9
+ designaround_core-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ designaround_core-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any