pywrit 0.1.4__tar.gz → 0.1.6__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.
- {pywrit-0.1.4/pywrit.egg-info → pywrit-0.1.6}/PKG-INFO +1 -1
- {pywrit-0.1.4 → pywrit-0.1.6}/pyproject.toml +1 -1
- {pywrit-0.1.4 → pywrit-0.1.6/pywrit.egg-info}/PKG-INFO +1 -1
- {pywrit-0.1.4 → pywrit-0.1.6}/pywrit.egg-info/SOURCES.txt +1 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/writ_cli/cli.py +118 -58
- pywrit-0.1.6/writ_cli/config.py +65 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/LICENSE +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/README.md +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/pywrit.egg-info/dependency_links.txt +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/pywrit.egg-info/entry_points.txt +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/pywrit.egg-info/requires.txt +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/pywrit.egg-info/top_level.txt +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/setup.cfg +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/tests/test_scan_tsjs.py +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/writ_cli/__init__.py +0 -0
- {pywrit-0.1.4 → pywrit-0.1.6}/writ_cli/risk.py +0 -0
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
# Distribution name: pywrit (approved by Chad 2026-09-27).
|
|
7
7
|
# The bare name "writ" is taken on PyPI.
|
|
8
8
|
name = "pywrit"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.6"
|
|
10
10
|
description = "Writ — the gate before the write. Allow/deny gate with a tamper-evident audit log"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.9"
|
|
@@ -15,30 +15,17 @@ import urllib.parse
|
|
|
15
15
|
import urllib.request
|
|
16
16
|
from datetime import datetime, timezone
|
|
17
17
|
|
|
18
|
+
try:
|
|
19
|
+
from writ_cli import config
|
|
20
|
+
except ImportError: # when cli.py is run directly as a script
|
|
21
|
+
import config
|
|
22
|
+
|
|
18
23
|
BASE = os.environ.get(
|
|
19
24
|
"WRIT_BASE",
|
|
20
|
-
"https://api.withwrit.com",
|
|
25
|
+
config.base_url() or "https://api.withwrit.com",
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
|
|
24
|
-
def request(method, path, payload=None, key=None, token=None):
|
|
25
|
-
headers = {"content-type": "application/json"}
|
|
26
|
-
if key:
|
|
27
|
-
headers["authorization"] = "Bearer " + key
|
|
28
|
-
elif token:
|
|
29
|
-
headers["authorization"] = token
|
|
30
|
-
data = None if payload is None else json.dumps(payload).encode()
|
|
31
|
-
req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
|
|
32
|
-
try:
|
|
33
|
-
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
34
|
-
body = resp.read().decode()
|
|
35
|
-
print(body)
|
|
36
|
-
return 0
|
|
37
|
-
except urllib.error.HTTPError as exc:
|
|
38
|
-
print(exc.read().decode(), file=sys.stderr)
|
|
39
|
-
return exc.code
|
|
40
|
-
|
|
41
|
-
|
|
42
29
|
def stream_cmd(args):
|
|
43
30
|
"""Tail the tenant audit log via the SSE stream, reconnecting on each
|
|
44
31
|
long-poll expiry. Prints one line per receipt."""
|
|
@@ -11227,6 +11214,34 @@ def request(method, path, payload=None, key=None, token=None):
|
|
|
11227
11214
|
return exc.code, exc.read().decode()
|
|
11228
11215
|
|
|
11229
11216
|
|
|
11217
|
+
def _request_and_print(method, path, payload=None, key=None, token=None):
|
|
11218
|
+
"""Call request(), print the response, and return a clean exit code.
|
|
11219
|
+
|
|
11220
|
+
Success bodies go to stdout; error bodies go to stderr. This is the
|
|
11221
|
+
helper all simple dispatchers should use so the CLI exits 0 on success.
|
|
11222
|
+
"""
|
|
11223
|
+
code, text = request(method, path, payload, key=key, token=token)
|
|
11224
|
+
if code == 0:
|
|
11225
|
+
print(text)
|
|
11226
|
+
return 0
|
|
11227
|
+
print(text, file=sys.stderr)
|
|
11228
|
+
return code
|
|
11229
|
+
|
|
11230
|
+
|
|
11231
|
+
def _require_key(key):
|
|
11232
|
+
if not key:
|
|
11233
|
+
print("error: no API key. Run `writ login --email you@example.com` or set WRIT_API_KEY.", file=sys.stderr)
|
|
11234
|
+
return False
|
|
11235
|
+
return True
|
|
11236
|
+
|
|
11237
|
+
|
|
11238
|
+
def _require_sponsor_token(token):
|
|
11239
|
+
if not token:
|
|
11240
|
+
print("error: no sponsor token. Run `writ login` or set WRIT_SPONSOR_TOKEN.", file=sys.stderr)
|
|
11241
|
+
return False
|
|
11242
|
+
return True
|
|
11243
|
+
|
|
11244
|
+
|
|
11230
11245
|
# ---------------------------------------------------------------------------
|
|
11231
11246
|
# Agent Action Report
|
|
11232
11247
|
# ---------------------------------------------------------------------------
|
|
@@ -11523,17 +11538,28 @@ def main():
|
|
|
11523
11538
|
key.add_argument("--tenant-name", default="", help="Optional tenant name (defaults to email domain)")
|
|
11524
11539
|
key.add_argument("--invite-code", default="", help="Join an existing tenant instead of creating one")
|
|
11525
11540
|
|
|
11541
|
+
login = sub.add_parser("login", help="Create or join a tenant and save credentials to ~/.writ/config.json")
|
|
11542
|
+
login.add_argument("--email", required=True)
|
|
11543
|
+
login.add_argument("--tenant-name", default="", help="Optional tenant name (defaults to email domain)")
|
|
11544
|
+
login.add_argument("--invite-code", default="", help="Join an existing tenant instead of creating one")
|
|
11545
|
+
|
|
11546
|
+
cfg = sub.add_parser("config", help="Read or write ~/.writ/config.json")
|
|
11547
|
+
cfg.add_argument("action", choices=("show", "get", "set", "remove"), default="show", nargs="?",
|
|
11548
|
+
help="Action (default: show)")
|
|
11549
|
+
cfg.add_argument("name", default="", help="Config key")
|
|
11550
|
+
cfg.add_argument("value", default="", help="Config value (for set)")
|
|
11551
|
+
|
|
11526
11552
|
me = sub.add_parser("me", help="Show tenant, key meta, and policy")
|
|
11527
|
-
me.add_argument("--key",
|
|
11553
|
+
me.add_argument("--key", default=config.api_key())
|
|
11528
11554
|
|
|
11529
11555
|
MODES = ("allow", "deny", "require_grant", "step_up")
|
|
11530
11556
|
policy = sub.add_parser("policy", help="Get or set tenant verb policy")
|
|
11531
|
-
policy.add_argument("--key",
|
|
11557
|
+
policy.add_argument("--key", default=config.api_key())
|
|
11532
11558
|
policy.add_argument("--set", nargs=2, metavar=("VERB", "MODE"), action="append", default=[])
|
|
11533
11559
|
policy.add_argument("--env", default="", help="Environment override to get/set: dev, staging, or prod (default: tenant policy)")
|
|
11534
11560
|
|
|
11535
11561
|
check = sub.add_parser("check", help="ALLOW or DENY a write")
|
|
11536
|
-
check.add_argument("--key",
|
|
11562
|
+
check.add_argument("--key", default=config.api_key())
|
|
11537
11563
|
check.add_argument("--sponsor", required=True)
|
|
11538
11564
|
check.add_argument("--agent", required=True)
|
|
11539
11565
|
check.add_argument("--verb", required=True)
|
|
@@ -11551,30 +11577,30 @@ def main():
|
|
|
11551
11577
|
revoke.add_argument("--sponsor", required=True)
|
|
11552
11578
|
revoke.add_argument("--agent", required=True)
|
|
11553
11579
|
revoke.add_argument("--reason", default="")
|
|
11554
|
-
revoke.add_argument("--sponsor-token", default=
|
|
11580
|
+
revoke.add_argument("--sponsor-token", default=config.sponsor_token())
|
|
11555
11581
|
|
|
11556
11582
|
reinstate = sub.add_parser("reinstate", help="Undo a revocation (sponsor token)")
|
|
11557
11583
|
reinstate.add_argument("--sponsor", required=True)
|
|
11558
11584
|
reinstate.add_argument("--agent", required=True)
|
|
11559
|
-
reinstate.add_argument("--sponsor-token", default=
|
|
11585
|
+
reinstate.add_argument("--sponsor-token", default=config.sponsor_token())
|
|
11560
11586
|
|
|
11561
11587
|
revoked = sub.add_parser("revoked", help="List revoked principals (sponsor token)")
|
|
11562
|
-
revoked.add_argument("--sponsor-token", default=
|
|
11588
|
+
revoked.add_argument("--sponsor-token", default=config.sponsor_token())
|
|
11563
11589
|
|
|
11564
11590
|
receipts = sub.add_parser("receipts", help="Latest receipts for your tenant")
|
|
11565
|
-
receipts.add_argument("--key",
|
|
11591
|
+
receipts.add_argument("--key", default=config.api_key())
|
|
11566
11592
|
|
|
11567
11593
|
stream = sub.add_parser("stream", help="Tail the live audit log (SSE stream)")
|
|
11568
|
-
stream.add_argument("--key",
|
|
11594
|
+
stream.add_argument("--key", default=config.api_key())
|
|
11569
11595
|
stream.add_argument("--since", default="", help="receiptId to resume after")
|
|
11570
11596
|
stream.add_argument("--timeout", type=int, default=600, help="Seconds to stream before exiting (0 = forever)")
|
|
11571
11597
|
|
|
11572
11598
|
verify = sub.add_parser("verify-chain", help="Verify the tenant audit-log hash chain")
|
|
11573
|
-
verify.add_argument("--key",
|
|
11599
|
+
verify.add_argument("--key", default=config.api_key())
|
|
11574
11600
|
verify.add_argument("--limit", type=int, default=200, help="Max receipts to verify (1-1000)")
|
|
11575
11601
|
|
|
11576
11602
|
invite = sub.add_parser("invite", help="Create a team invite code for your tenant")
|
|
11577
|
-
invite.add_argument("--key",
|
|
11603
|
+
invite.add_argument("--key", default=config.api_key())
|
|
11578
11604
|
|
|
11579
11605
|
grant = sub.add_parser("grant", help="Sponsor approval: grant one write (sponsor token)")
|
|
11580
11606
|
grant.add_argument("--sponsor", required=True)
|
|
@@ -11583,10 +11609,10 @@ def main():
|
|
|
11583
11609
|
grant.add_argument("--target", required=True)
|
|
11584
11610
|
grant.add_argument("--purpose", required=True)
|
|
11585
11611
|
grant.add_argument("--ttl", type=int, default=90, help="Seconds the grant stays valid (1-3600)")
|
|
11586
|
-
grant.add_argument("--sponsor-token", default=
|
|
11612
|
+
grant.add_argument("--sponsor-token", default=config.sponsor_token())
|
|
11587
11613
|
|
|
11588
11614
|
tv = sub.add_parser("token-verify", help="Validate an ALLOW auth token")
|
|
11589
|
-
tv.add_argument("--key",
|
|
11615
|
+
tv.add_argument("--key", default=config.api_key())
|
|
11590
11616
|
tv.add_argument("--token", required=True)
|
|
11591
11617
|
tv.add_argument("--verb", default=None)
|
|
11592
11618
|
tv.add_argument("--target", default=None)
|
|
@@ -11610,7 +11636,7 @@ def main():
|
|
|
11610
11636
|
help="Fetch the tenant's verb manifest (GET /v1/verbs) and "
|
|
11611
11637
|
"map inferred verbs onto custom verbs when the action "
|
|
11612
11638
|
"matches exactly one (needs --key)")
|
|
11613
|
-
scan.add_argument("--key", default=
|
|
11639
|
+
scan.add_argument("--key", default=config.api_key(),
|
|
11614
11640
|
help="API key (for --push-policy / --manifest-out)")
|
|
11615
11641
|
scan.add_argument("--manifest-out", default="",
|
|
11616
11642
|
help="Write a signed CI attestation manifest to PATH")
|
|
@@ -11623,7 +11649,7 @@ def main():
|
|
|
11623
11649
|
|
|
11624
11650
|
report = sub.add_parser("report", help="Agent Action Report from receipts")
|
|
11625
11651
|
rsrc = report.add_mutually_exclusive_group(required=True)
|
|
11626
|
-
rsrc.add_argument("--key", help="API key — fetch receipts from the Writ API")
|
|
11652
|
+
rsrc.add_argument("--key", default=config.api_key(), help="API key — fetch receipts from the Writ API")
|
|
11627
11653
|
rsrc.add_argument("--file", help="Read receipts from a local JSON file (array or JSON-lines)")
|
|
11628
11654
|
report.add_argument("--agent", help="Filter to one agent ID")
|
|
11629
11655
|
report.add_argument("--since", help="Only receipts after this date (YYYY-MM-DD) or unix timestamp")
|
|
@@ -11634,7 +11660,7 @@ def main():
|
|
|
11634
11660
|
help="Output format (default: text)")
|
|
11635
11661
|
|
|
11636
11662
|
manifest_key = sub.add_parser("manifest-key", help="Fetch the tenant's CI manifest HMAC key")
|
|
11637
|
-
manifest_key.add_argument("--key",
|
|
11663
|
+
manifest_key.add_argument("--key", default=config.api_key())
|
|
11638
11664
|
|
|
11639
11665
|
args = parser.parse_args()
|
|
11640
11666
|
if args.cmd == "key":
|
|
@@ -11643,14 +11669,54 @@ def main():
|
|
|
11643
11669
|
payload["tenantName"] = args.tenant_name.strip()
|
|
11644
11670
|
if args.invite_code.strip():
|
|
11645
11671
|
payload["inviteCode"] = args.invite_code.strip()
|
|
11646
|
-
return
|
|
11672
|
+
return _request_and_print("POST", "/v1/keys", payload)
|
|
11673
|
+
if args.cmd == "login":
|
|
11674
|
+
payload = {"email": args.email}
|
|
11675
|
+
if args.tenant_name.strip():
|
|
11676
|
+
payload["tenantName"] = args.tenant_name.strip()
|
|
11677
|
+
if args.invite_code.strip():
|
|
11678
|
+
payload["inviteCode"] = args.invite_code.strip()
|
|
11679
|
+
code, text = request("POST", "/v1/keys", payload)
|
|
11680
|
+
if code != 0:
|
|
11681
|
+
print(text, file=sys.stderr)
|
|
11682
|
+
return code
|
|
11683
|
+
data = json.loads(text)
|
|
11684
|
+
config.set_item("apiKey", data.get("apiKey", ""))
|
|
11685
|
+
config.set_item("sponsorToken", data.get("sponsorToken", ""))
|
|
11686
|
+
if data.get("tenantId"):
|
|
11687
|
+
print("Tenant: %s" % data["tenantId"])
|
|
11688
|
+
print("API key and sponsor token saved to ~/.writ/config.json")
|
|
11689
|
+
return 0
|
|
11690
|
+
if args.cmd == "config":
|
|
11691
|
+
if args.action == "show":
|
|
11692
|
+
print(json.dumps(config.read_config(), indent=2))
|
|
11693
|
+
elif args.action == "get":
|
|
11694
|
+
print(config.get(args.name, ""))
|
|
11695
|
+
elif args.action == "set":
|
|
11696
|
+
if not args.name:
|
|
11697
|
+
print("error: config set needs a name.", file=sys.stderr)
|
|
11698
|
+
return 2
|
|
11699
|
+
config.set_item(args.name, args.value)
|
|
11700
|
+
print("set %s" % args.name)
|
|
11701
|
+
elif args.action == "remove":
|
|
11702
|
+
if not args.name:
|
|
11703
|
+
print("error: config remove needs a name.", file=sys.stderr)
|
|
11704
|
+
return 2
|
|
11705
|
+
config.set_item(args.name, None)
|
|
11706
|
+
print("removed %s" % args.name)
|
|
11707
|
+
return 0
|
|
11708
|
+
|
|
11709
|
+
_KEY_COMMANDS = {"me", "policy", "check", "receipts", "stream",
|
|
11710
|
+
"verify-chain", "invite", "token-verify", "manifest-key"}
|
|
11711
|
+
if args.cmd in _KEY_COMMANDS and not _require_key(args.key):
|
|
11712
|
+
return 2
|
|
11713
|
+
|
|
11647
11714
|
if args.cmd == "invite":
|
|
11648
|
-
return
|
|
11715
|
+
return _request_and_print("POST", "/v1/tenant/invites", key=args.key)
|
|
11649
11716
|
if args.cmd == "grant":
|
|
11650
|
-
if not args.sponsor_token:
|
|
11651
|
-
print("error: sponsor token required (--sponsor-token or WRIT_SPONSOR_TOKEN)", file=sys.stderr)
|
|
11717
|
+
if not _require_sponsor_token(args.sponsor_token):
|
|
11652
11718
|
return 2
|
|
11653
|
-
return
|
|
11719
|
+
return _request_and_print(
|
|
11654
11720
|
"POST",
|
|
11655
11721
|
"/v1/grants",
|
|
11656
11722
|
{
|
|
@@ -11664,7 +11730,7 @@ def main():
|
|
|
11664
11730
|
token="Bearer " + args.sponsor_token,
|
|
11665
11731
|
)
|
|
11666
11732
|
if args.cmd == "me":
|
|
11667
|
-
return
|
|
11733
|
+
return _request_and_print("GET", "/v1/me", key=args.key)
|
|
11668
11734
|
if args.cmd == "policy":
|
|
11669
11735
|
url = "/v1/policy" if not args.env else f"/v1/policies/environments/{args.env}"
|
|
11670
11736
|
if args.set:
|
|
@@ -11672,45 +11738,39 @@ def main():
|
|
|
11672
11738
|
if mode not in MODES:
|
|
11673
11739
|
print(f"error: mode must be one of {', '.join(MODES)}", file=sys.stderr)
|
|
11674
11740
|
return 2
|
|
11675
|
-
return
|
|
11676
|
-
return
|
|
11741
|
+
return _request_and_print("PUT", url, {verb: mode for verb, mode in args.set}, key=args.key)
|
|
11742
|
+
return _request_and_print("GET", url, key=args.key)
|
|
11677
11743
|
if args.cmd == "receipts":
|
|
11678
|
-
return
|
|
11744
|
+
return _request_and_print("GET", "/v1/receipts", key=args.key)
|
|
11679
11745
|
if args.cmd == "stream":
|
|
11680
11746
|
return stream_cmd(args)
|
|
11681
11747
|
if args.cmd == "verify-chain":
|
|
11682
|
-
return
|
|
11748
|
+
return _request_and_print("GET", f"/v1/receipts/verify?limit={args.limit}", key=args.key)
|
|
11683
11749
|
if args.cmd == "token-verify":
|
|
11684
11750
|
payload = {"token": args.token}
|
|
11685
11751
|
for field in ("verb", "target", "purpose"):
|
|
11686
11752
|
value = getattr(args, field)
|
|
11687
11753
|
if value is not None:
|
|
11688
11754
|
payload[field] = value
|
|
11689
|
-
return
|
|
11755
|
+
return _request_and_print("POST", "/v1/tokens/verify", payload, key=args.key)
|
|
11690
11756
|
if args.cmd == "scan":
|
|
11691
11757
|
return scan_cmd(args)
|
|
11692
11758
|
if args.cmd == "report":
|
|
11693
11759
|
return report_cmd(args)
|
|
11694
11760
|
if args.cmd == "manifest-key":
|
|
11695
|
-
|
|
11696
|
-
if code == 0:
|
|
11697
|
-
print(text)
|
|
11698
|
-
return 0
|
|
11699
|
-
print(text, file=sys.stderr)
|
|
11700
|
-
return code
|
|
11761
|
+
return _request_and_print("GET", "/v1/manifest-key", key=args.key)
|
|
11701
11762
|
if args.cmd in ("revoke", "reinstate", "revoked"):
|
|
11702
|
-
if not args.sponsor_token:
|
|
11703
|
-
print("error: sponsor token required (--sponsor-token or WRIT_SPONSOR_TOKEN)", file=sys.stderr)
|
|
11763
|
+
if not _require_sponsor_token(args.sponsor_token):
|
|
11704
11764
|
return 2
|
|
11705
11765
|
token = "Bearer " + args.sponsor_token
|
|
11706
11766
|
if args.cmd == "revoked":
|
|
11707
|
-
return
|
|
11767
|
+
return _request_and_print("GET", "/v1/revoked", token=token)
|
|
11708
11768
|
payload = {"sponsorId": args.sponsor, "agentId": args.agent}
|
|
11709
11769
|
if args.cmd == "revoke":
|
|
11710
11770
|
if args.reason:
|
|
11711
11771
|
payload["reason"] = args.reason
|
|
11712
|
-
return
|
|
11713
|
-
return
|
|
11772
|
+
return _request_and_print("POST", "/v1/revoke", payload, token=token)
|
|
11773
|
+
return _request_and_print("POST", "/v1/reinstate", payload, token=token)
|
|
11714
11774
|
body = {
|
|
11715
11775
|
"sponsorId": args.sponsor,
|
|
11716
11776
|
"agentId": args.agent,
|
|
@@ -11721,8 +11781,8 @@ def main():
|
|
|
11721
11781
|
if args.cmd == "check" and args.environment:
|
|
11722
11782
|
body["environment"] = args.environment
|
|
11723
11783
|
if args.cmd == "sandbox":
|
|
11724
|
-
return
|
|
11725
|
-
return
|
|
11784
|
+
return _request_and_print("POST", "/v1/sandbox", body)
|
|
11785
|
+
return _request_and_print("POST", "/v1/check", body, key=args.key)
|
|
11726
11786
|
|
|
11727
11787
|
|
|
11728
11788
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Local CLI configuration: ~/.writ/config.json.
|
|
2
|
+
|
|
3
|
+
The CLI looks up credentials in this order:
|
|
4
|
+
1. Explicit flags (--key, --sponsor-token)
|
|
5
|
+
2. Environment variables (WRIT_API_KEY, WRIT_SPONSOR_TOKEN, WRIT_BASE)
|
|
6
|
+
3. ~/.writ/config.json (apiKey, sponsorToken, baseUrl)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _config_dir():
|
|
14
|
+
home = os.path.expanduser("~")
|
|
15
|
+
d = os.path.join(home, ".writ")
|
|
16
|
+
os.makedirs(d, exist_ok=True, mode=0o700)
|
|
17
|
+
return d
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _config_path():
|
|
21
|
+
return os.path.join(_config_dir(), "config.json")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_config():
|
|
25
|
+
path = _config_path()
|
|
26
|
+
if not os.path.exists(path):
|
|
27
|
+
return {}
|
|
28
|
+
try:
|
|
29
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
30
|
+
return json.load(f)
|
|
31
|
+
except (ValueError, OSError):
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_config(cfg):
|
|
36
|
+
path = _config_path()
|
|
37
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
38
|
+
json.dump(cfg, f, indent=2)
|
|
39
|
+
f.write("\n")
|
|
40
|
+
os.chmod(path, 0o600)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get(key, default=None):
|
|
44
|
+
return read_config().get(key, default)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def set_item(key, value):
|
|
48
|
+
cfg = read_config()
|
|
49
|
+
if value is None:
|
|
50
|
+
cfg.pop(key, None)
|
|
51
|
+
else:
|
|
52
|
+
cfg[key] = value
|
|
53
|
+
write_config(cfg)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def api_key():
|
|
57
|
+
return os.environ.get("WRIT_API_KEY") or get("apiKey", "")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sponsor_token():
|
|
61
|
+
return os.environ.get("WRIT_SPONSOR_TOKEN") or get("sponsorToken", "")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def base_url():
|
|
65
|
+
return os.environ.get("WRIT_BASE") or get("baseUrl", "")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|