langchain-atomicmail 0.3.14__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.
Files changed (49) hide show
  1. langchain_atomicmail-0.3.14/PKG-INFO +136 -0
  2. langchain_atomicmail-0.3.14/README.md +110 -0
  3. langchain_atomicmail-0.3.14/pyproject.toml +59 -0
  4. langchain_atomicmail-0.3.14/setup.cfg +4 -0
  5. langchain_atomicmail-0.3.14/src/atomicmail/__init__.py +32 -0
  6. langchain_atomicmail-0.3.14/src/atomicmail/auth_http.py +177 -0
  7. langchain_atomicmail-0.3.14/src/atomicmail/cli.py +236 -0
  8. langchain_atomicmail-0.3.14/src/atomicmail/config.py +112 -0
  9. langchain_atomicmail-0.3.14/src/atomicmail/constants.py +32 -0
  10. langchain_atomicmail-0.3.14/src/atomicmail/credentials.py +158 -0
  11. langchain_atomicmail-0.3.14/src/atomicmail/help.py +118 -0
  12. langchain_atomicmail-0.3.14/src/atomicmail/jmap_request.py +918 -0
  13. langchain_atomicmail-0.3.14/src/atomicmail/jwt_utils.py +34 -0
  14. langchain_atomicmail-0.3.14/src/atomicmail/mcp_server.py +341 -0
  15. langchain_atomicmail-0.3.14/src/atomicmail/pow.py +71 -0
  16. langchain_atomicmail-0.3.14/src/atomicmail/session.py +595 -0
  17. langchain_atomicmail-0.3.14/src/atomicmail/shared_assets.py +67 -0
  18. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/consts.json +11 -0
  19. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/fixtures/pow_vectors.json +32 -0
  20. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/fragments/inbox_cron_agent_prompt.md +1 -0
  21. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/fragments/post_register_cron_reminder.md +5 -0
  22. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/readme_stub.md +3 -0
  23. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/auth.md +8 -0
  24. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/cron.md +217 -0
  25. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/installation.md +35 -0
  26. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/jmap_cheatsheet.md +19 -0
  27. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/multi_account.md +9 -0
  28. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/overview.md +27 -0
  29. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/presets.md +12 -0
  30. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/tools.md +16 -0
  31. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/troubleshooting.md +6 -0
  32. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/manifest.json +31 -0
  33. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/messages/errors.json +68 -0
  34. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/messages/hints.json +8 -0
  35. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/list_inbox.json +46 -0
  36. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/reply.json +97 -0
  37. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/send_mail.json +70 -0
  38. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/send_mail_attachment.json +92 -0
  39. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/send_mail_blob_attachment.json +74 -0
  40. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/skill/SKILL.template.md +202 -0
  41. langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/skill/manifest.json +89 -0
  42. langchain_atomicmail-0.3.14/src/langchain_atomicmail/__init__.py +23 -0
  43. langchain_atomicmail-0.3.14/src/langchain_atomicmail/toolkit.py +15 -0
  44. langchain_atomicmail-0.3.14/src/langchain_atomicmail/tools.py +251 -0
  45. langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/PKG-INFO +136 -0
  46. langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/SOURCES.txt +47 -0
  47. langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/dependency_links.txt +1 -0
  48. langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/requires.txt +4 -0
  49. langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/top_level.txt +2 -0
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-atomicmail
3
+ Version: 0.3.14
4
+ Summary: LangChain tools for Atomic Mail register/help/JMAP operations
5
+ Author: Atomic Mail
6
+ License: MIT
7
+ Project-URL: Homepage, https://atomicmail.ai
8
+ Project-URL: Source, https://github.com/Atomic-Mail/atomic-mail-agentic
9
+ Project-URL: Documentation, https://atomic-mail.github.io/atomic-mail-agentic/langchain
10
+ Keywords: atomic-mail,atomicmail,langchain,toolkit,agent,ai,jmap,email,proof-of-work
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Communications :: Email
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: langchain-core<2,>=0.3.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+
27
+ # langchain-atomicmail
28
+
29
+ LangChain integration package for Atomic Mail.
30
+
31
+ It exposes three tools backed by the same Python Atomic Mail runtime used by the
32
+ CLI/MCP adapters:
33
+
34
+ - `register`
35
+ - `jmap_request`
36
+ - `help`
37
+
38
+ ## Install
39
+
40
+ Published package:
41
+
42
+ ```bash
43
+ pip install langchain-atomicmail
44
+ ```
45
+
46
+ The published wheel bundles the Atomic Mail Python runtime and shared presets; no
47
+ separate PyPI package is required.
48
+
49
+ Local development in this monorepo (install core library first):
50
+
51
+ ```bash
52
+ cd py
53
+ pip install -e .
54
+ pip install -e langchain
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ```python
60
+ from langchain_atomicmail import AtomicMailToolkit
61
+
62
+ toolkit = AtomicMailToolkit()
63
+ tools = toolkit.get_tools()
64
+ ```
65
+
66
+ You can also import tool factories directly:
67
+
68
+ ```python
69
+ from langchain_atomicmail import get_atomicmail_tools
70
+
71
+ tools = get_atomicmail_tools()
72
+ ```
73
+
74
+ ## Agent example
75
+
76
+ Use the toolkit with a ReAct agent (requires `langgraph` and a chat model):
77
+
78
+ ```python
79
+ from langchain_openai import ChatOpenAI
80
+ from langgraph.prebuilt import create_react_agent
81
+
82
+ from langchain_atomicmail import AtomicMailToolkit
83
+
84
+ llm = ChatOpenAI(temperature=0)
85
+ toolkit = AtomicMailToolkit()
86
+ agent = create_react_agent(llm, toolkit.get_tools())
87
+
88
+ # Register an inbox, then fetch mail
89
+ agent.invoke({"messages": [("user", "Register username myagent and list my inbox")]})
90
+ ```
91
+
92
+ After a successful `register`, the tool response includes `_next` guidance for
93
+ hourly inbox polling. Call `help` with topic `cron` for full scheduling details.
94
+
95
+ ## Inbox polling (after register)
96
+
97
+ Registration only creates credentials. For a useful inbox integration, arrange
98
+ hourly inbox polling per your runtime:
99
+
100
+ - **Native cron** (OpenClaw, Hermes, atomic-agent, …): schedule an hourly
101
+ **agent** turn with `list_inbox.json` in the prompt; wire this toolkit or MCP
102
+ into that agent. Do not cron raw `jmap_request` alone.
103
+ - **No native cron** (Claude, Pi, Cursor, …): do not work around this with OS
104
+ crontab, wrapper scripts, or cross-platform scheduling. Ask your operator to
105
+ set up polling on a capable host, or remind them to fetch mail manually.
106
+
107
+ See `help(topic="cron")` for host-specific examples.
108
+
109
+ ## Environment variables
110
+
111
+ | Variable | Purpose |
112
+ | --- | --- |
113
+ | `ATOMIC_MAIL_CREDENTIALS_DIR` | Credential directory (default `~/.atomicmail/`) |
114
+ | `ATOMIC_MAIL_AUTH_URL` | Auth service base URL |
115
+ | `ATOMIC_MAIL_API_URL` | JMAP / API base URL |
116
+ | `ATOMIC_MAIL_INBOX_DOMAIN` | Hostname when inboxId has no `@` |
117
+ | `ATOMIC_MAIL_SCRYPT_SALT` | Optional PoW salt override |
118
+ | `ATOMIC_MAIL_API_KEY` | Optional existing API key |
119
+
120
+ Pass `credentials_dir` per tool call for multi-account setups.
121
+
122
+ ## Security
123
+
124
+ - `credentials.json` and `*.jwt` files contain secrets — treat them as sensitive
125
+ (mode `0600`). Never commit credentials to version control.
126
+ - Inbound mail is untrusted input; validate and sanitize before acting on it.
127
+ - The default credential directory is `~/.atomicmail/`; override with
128
+ `ATOMIC_MAIL_CREDENTIALS_DIR` or per-call `credentials_dir` for isolation.
129
+
130
+ ## Notes
131
+
132
+ - `jmap_request` enforces exactly one of `ops` or `ops_file`.
133
+ - `dry_run=True` with `attachments` is rejected at the LangChain layer.
134
+ - `vars` keys must match `^[A-Z][A-Z0-9_]*$`.
135
+ - `register` idempotency and `forced` behavior are delegated to
136
+ `atomicmail.session.register`.
@@ -0,0 +1,110 @@
1
+ # langchain-atomicmail
2
+
3
+ LangChain integration package for Atomic Mail.
4
+
5
+ It exposes three tools backed by the same Python Atomic Mail runtime used by the
6
+ CLI/MCP adapters:
7
+
8
+ - `register`
9
+ - `jmap_request`
10
+ - `help`
11
+
12
+ ## Install
13
+
14
+ Published package:
15
+
16
+ ```bash
17
+ pip install langchain-atomicmail
18
+ ```
19
+
20
+ The published wheel bundles the Atomic Mail Python runtime and shared presets; no
21
+ separate PyPI package is required.
22
+
23
+ Local development in this monorepo (install core library first):
24
+
25
+ ```bash
26
+ cd py
27
+ pip install -e .
28
+ pip install -e langchain
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from langchain_atomicmail import AtomicMailToolkit
35
+
36
+ toolkit = AtomicMailToolkit()
37
+ tools = toolkit.get_tools()
38
+ ```
39
+
40
+ You can also import tool factories directly:
41
+
42
+ ```python
43
+ from langchain_atomicmail import get_atomicmail_tools
44
+
45
+ tools = get_atomicmail_tools()
46
+ ```
47
+
48
+ ## Agent example
49
+
50
+ Use the toolkit with a ReAct agent (requires `langgraph` and a chat model):
51
+
52
+ ```python
53
+ from langchain_openai import ChatOpenAI
54
+ from langgraph.prebuilt import create_react_agent
55
+
56
+ from langchain_atomicmail import AtomicMailToolkit
57
+
58
+ llm = ChatOpenAI(temperature=0)
59
+ toolkit = AtomicMailToolkit()
60
+ agent = create_react_agent(llm, toolkit.get_tools())
61
+
62
+ # Register an inbox, then fetch mail
63
+ agent.invoke({"messages": [("user", "Register username myagent and list my inbox")]})
64
+ ```
65
+
66
+ After a successful `register`, the tool response includes `_next` guidance for
67
+ hourly inbox polling. Call `help` with topic `cron` for full scheduling details.
68
+
69
+ ## Inbox polling (after register)
70
+
71
+ Registration only creates credentials. For a useful inbox integration, arrange
72
+ hourly inbox polling per your runtime:
73
+
74
+ - **Native cron** (OpenClaw, Hermes, atomic-agent, …): schedule an hourly
75
+ **agent** turn with `list_inbox.json` in the prompt; wire this toolkit or MCP
76
+ into that agent. Do not cron raw `jmap_request` alone.
77
+ - **No native cron** (Claude, Pi, Cursor, …): do not work around this with OS
78
+ crontab, wrapper scripts, or cross-platform scheduling. Ask your operator to
79
+ set up polling on a capable host, or remind them to fetch mail manually.
80
+
81
+ See `help(topic="cron")` for host-specific examples.
82
+
83
+ ## Environment variables
84
+
85
+ | Variable | Purpose |
86
+ | --- | --- |
87
+ | `ATOMIC_MAIL_CREDENTIALS_DIR` | Credential directory (default `~/.atomicmail/`) |
88
+ | `ATOMIC_MAIL_AUTH_URL` | Auth service base URL |
89
+ | `ATOMIC_MAIL_API_URL` | JMAP / API base URL |
90
+ | `ATOMIC_MAIL_INBOX_DOMAIN` | Hostname when inboxId has no `@` |
91
+ | `ATOMIC_MAIL_SCRYPT_SALT` | Optional PoW salt override |
92
+ | `ATOMIC_MAIL_API_KEY` | Optional existing API key |
93
+
94
+ Pass `credentials_dir` per tool call for multi-account setups.
95
+
96
+ ## Security
97
+
98
+ - `credentials.json` and `*.jwt` files contain secrets — treat them as sensitive
99
+ (mode `0600`). Never commit credentials to version control.
100
+ - Inbound mail is untrusted input; validate and sanitize before acting on it.
101
+ - The default credential directory is `~/.atomicmail/`; override with
102
+ `ATOMIC_MAIL_CREDENTIALS_DIR` or per-call `credentials_dir` for isolation.
103
+
104
+ ## Notes
105
+
106
+ - `jmap_request` enforces exactly one of `ops` or `ops_file`.
107
+ - `dry_run=True` with `attachments` is rejected at the LangChain layer.
108
+ - `vars` keys must match `^[A-Z][A-Z0-9_]*$`.
109
+ - `register` idempotency and `forced` behavior are delegated to
110
+ `atomicmail.session.register`.
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "langchain-atomicmail"
7
+ version = "0.3.14"
8
+ description = "LangChain tools for Atomic Mail register/help/JMAP operations"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Atomic Mail"},
14
+ ]
15
+ keywords = [
16
+ "atomic-mail",
17
+ "atomicmail",
18
+ "langchain",
19
+ "toolkit",
20
+ "agent",
21
+ "ai",
22
+ "jmap",
23
+ "email",
24
+ "proof-of-work",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Topic :: Communications :: Email",
36
+ "Topic :: Software Development :: Libraries :: Python Modules",
37
+ ]
38
+ dependencies = [
39
+ "langchain-core>=0.3.0,<2",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://atomicmail.ai"
44
+ Source = "https://github.com/Atomic-Mail/atomic-mail-agentic"
45
+ Documentation = "https://atomic-mail.github.io/atomic-mail-agentic/langchain"
46
+
47
+ [project.optional-dependencies]
48
+ dev = ["pytest"]
49
+
50
+ [tool.setuptools]
51
+
52
+ [tool.setuptools.package-dir]
53
+ "" = "src"
54
+
55
+ [tool.setuptools.packages.find]
56
+ where = ["src"]
57
+
58
+ [tool.setuptools.package-data]
59
+ atomicmail = ["vendor/shared/**/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ """Atomic Mail Python shared foundation package."""
2
+
3
+ from .config import ResolvedAgentConfig, resolve_agent_config_from_env
4
+ from .credentials import (
5
+ CredentialStore,
6
+ Credentials,
7
+ SkillFiles,
8
+ default_files_from_out_dir,
9
+ )
10
+ from .help import help
11
+ from .jmap_request import JmapAttachmentInput, JmapRequestResult, jmap_request, run_jmap_request
12
+ from .mcp_server import handle_tool_call
13
+ from .session import AgentSession, RegisterResult, create_agent_session, register
14
+
15
+ __all__ = [
16
+ "AgentSession",
17
+ "Credentials",
18
+ "CredentialStore",
19
+ "JmapRequestResult",
20
+ "JmapAttachmentInput",
21
+ "RegisterResult",
22
+ "ResolvedAgentConfig",
23
+ "SkillFiles",
24
+ "default_files_from_out_dir",
25
+ "help",
26
+ "handle_tool_call",
27
+ "jmap_request",
28
+ "run_jmap_request",
29
+ "register",
30
+ "create_agent_session",
31
+ "resolve_agent_config_from_env",
32
+ ]
@@ -0,0 +1,177 @@
1
+ """Auth-service HTTP helpers: challenge -> session -> capability."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Callable, Mapping
8
+ from urllib.error import HTTPError
9
+ from urllib.request import Request, urlopen
10
+
11
+ from .jwt_utils import decode_jwt_payload
12
+ from .pow import solve_pow
13
+
14
+
15
+ @dataclass
16
+ class ChallengeResponse:
17
+ challengeJWT: str
18
+ challenge: str
19
+ difficulty: int
20
+
21
+
22
+ @dataclass
23
+ class SessionResponse:
24
+ sessionJWT: str
25
+ apiKey: str | None = None
26
+
27
+
28
+ def fetch_challenge(auth_url: str) -> ChallengeResponse:
29
+ base = auth_url.rstrip("/")
30
+ status, text, headers = _http_post(f"{base}/api/v1/challenge")
31
+ if status < 200 or status >= 300:
32
+ raise ValueError(
33
+ f"auth-service /api/v1/challenge returned {status}: {text}"
34
+ )
35
+
36
+ challenge_jwt = _read_bearer_token(
37
+ headers.get("Authorization"),
38
+ "Challenge response missing Authorization bearer token.",
39
+ )
40
+ payload = decode_jwt_payload(challenge_jwt)
41
+ challenge = payload.get("jti")
42
+ difficulty = payload.get("difficulty")
43
+ if not isinstance(challenge, str) or not isinstance(difficulty, (int, float)):
44
+ raise ValueError("Challenge JWT payload malformed (missing jti or difficulty).")
45
+
46
+ return ChallengeResponse(
47
+ challengeJWT=challenge_jwt,
48
+ challenge=challenge,
49
+ difficulty=int(difficulty),
50
+ )
51
+
52
+
53
+ def exchange_session(
54
+ auth_url: str,
55
+ *,
56
+ challenge_jwt: str,
57
+ pow_hex: str,
58
+ nonce: str,
59
+ api_key: str | None = None,
60
+ username: str | None = None,
61
+ ) -> SessionResponse:
62
+ base = auth_url.rstrip("/")
63
+ payload: dict[str, str] = {"powHex": pow_hex, "nonce": nonce}
64
+ if api_key:
65
+ payload["apiKey"] = api_key
66
+ if username:
67
+ payload["username"] = username
68
+
69
+ status, text, headers = _http_post(
70
+ f"{base}/api/v1/session",
71
+ headers={"Authorization": f"Bearer {challenge_jwt}"},
72
+ json_body=payload,
73
+ )
74
+ if status < 200 or status >= 300:
75
+ raise ValueError(f"auth-service /api/v1/session returned {status}: {text}")
76
+
77
+ session_jwt = _read_bearer_token(
78
+ headers.get("Authorization"),
79
+ "Session response missing Authorization bearer token.",
80
+ )
81
+
82
+ data: dict[str, object] = {}
83
+ if text.strip():
84
+ try:
85
+ parsed = json.loads(text)
86
+ except json.JSONDecodeError as err:
87
+ raise ValueError(
88
+ "auth-service /api/v1/session returned non-JSON body."
89
+ ) from err
90
+ if not isinstance(parsed, dict):
91
+ raise ValueError("auth-service /api/v1/session returned non-JSON body.")
92
+ data = parsed
93
+
94
+ api_key_out = data.get("apiKey")
95
+ return SessionResponse(
96
+ sessionJWT=session_jwt,
97
+ apiKey=api_key_out if isinstance(api_key_out, str) else None,
98
+ )
99
+
100
+
101
+ def fetch_capability(auth_url: str, session_jwt: str) -> str:
102
+ base = auth_url.rstrip("/")
103
+ status, text, headers = _http_post(
104
+ f"{base}/api/v1/capability",
105
+ headers={"Authorization": f"Bearer {session_jwt}"},
106
+ )
107
+ if status < 200 or status >= 300:
108
+ raise ValueError(f"auth-service /api/v1/capability returned {status}: {text}")
109
+
110
+ return _read_bearer_token(
111
+ headers.get("Authorization"),
112
+ "Capability response missing Authorization bearer token.",
113
+ )
114
+
115
+
116
+ def perform_pow_and_session(
117
+ *,
118
+ auth_url: str,
119
+ scrypt_salt: str,
120
+ api_key: str | None = None,
121
+ username: str | None = None,
122
+ on_pow_progress: Callable[[int], None] | None = None,
123
+ ) -> SessionResponse:
124
+ challenge = fetch_challenge(auth_url)
125
+ solved = solve_pow(
126
+ challenge=challenge.challenge,
127
+ difficulty=challenge.difficulty,
128
+ salt=scrypt_salt,
129
+ on_progress=on_pow_progress,
130
+ )
131
+ return exchange_session(
132
+ auth_url,
133
+ challenge_jwt=challenge.challengeJWT,
134
+ pow_hex=solved.powHex,
135
+ nonce=solved.nonce,
136
+ api_key=api_key,
137
+ username=username,
138
+ )
139
+
140
+
141
+ def _read_bearer_token(header_value: str | None, missing_error: str) -> str:
142
+ if not header_value:
143
+ raise ValueError(missing_error)
144
+
145
+ raw = header_value.strip()
146
+ prefix = "bearer "
147
+ if not raw.lower().startswith(prefix):
148
+ raise ValueError("Authorization header must use Bearer scheme.")
149
+
150
+ token = raw[len(prefix) :].strip()
151
+ if not token:
152
+ raise ValueError("Authorization header must use Bearer scheme.")
153
+ return token
154
+
155
+
156
+ def _http_post(
157
+ url: str,
158
+ *,
159
+ headers: Mapping[str, str] | None = None,
160
+ json_body: object | None = None,
161
+ ) -> tuple[int, str, Mapping[str, str]]:
162
+ req_headers = dict(headers or {})
163
+ body_bytes: bytes | None = None
164
+ if json_body is not None:
165
+ body_bytes = json.dumps(json_body).encode("utf-8")
166
+ req_headers.setdefault("Content-Type", "application/json")
167
+
168
+ req = Request(url, data=body_bytes, headers=req_headers, method="POST")
169
+ try:
170
+ with urlopen(req) as response:
171
+ return (
172
+ int(response.getcode()),
173
+ response.read().decode("utf-8"),
174
+ response.headers,
175
+ )
176
+ except HTTPError as err:
177
+ return err.code, err.read().decode("utf-8", errors="replace"), err.headers