memwal 0.1.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ wheels/
12
+ *.whl
13
+ .eggs/
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+ env/
19
+ ENV/
20
+
21
+ # Test / coverage
22
+ .pytest_cache/
23
+ .coverage
24
+ .coverage.*
25
+ coverage.xml
26
+ htmlcov/
27
+ .tox/
28
+
29
+ # Type checking
30
+ .mypy_cache/
31
+ .dmypy.json
32
+ dmypy.json
33
+ .pyright/
34
+
35
+ # Linting
36
+ .ruff_cache/
37
+
38
+ # Distribution / packaging
39
+ MANIFEST
40
+ pip-wheel-metadata/
41
+ share/python-wheels/
42
+
43
+ # IDE
44
+ .vscode/
45
+ .idea/
46
+ *.swp
47
+ *.swo
48
+ *~
49
+
50
+ # macOS
51
+ .DS_Store
52
+ .AppleDouble
53
+ .LSOverride
54
+
55
+ # Env files
56
+ .env
57
+ .env.*
58
+ !.env.example
59
+
60
+ # Jupyter
61
+ .ipynb_checkpoints/
62
+ *.ipynb
63
+
64
+ # Docs build
65
+ site/
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: memwal
3
+ Version: 0.1.0.dev0
4
+ Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
5
+ Project-URL: Homepage, https://memwal.ai
6
+ Project-URL: Documentation, https://docs.memwal.ai
7
+ Project-URL: Repository, https://github.com/MystenLabs/MemWal
8
+ Author: MemWal Team
9
+ License-Expression: MIT
10
+ Keywords: ai,ed25519,memory,memwal,privacy,sui,walrus
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 :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: httpx>=0.27.0
24
+ Requires-Dist: pynacl>=1.5.0
25
+ Provides-Extra: all
26
+ Requires-Dist: langchain-core>=0.2.0; extra == 'all'
27
+ Requires-Dist: openai>=1.0.0; extra == 'all'
28
+ Provides-Extra: dev
29
+ Requires-Dist: langchain-core>=0.2.0; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
31
+ Requires-Dist: pytest>=7.0; extra == 'dev'
32
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
34
+ Provides-Extra: langchain
35
+ Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
36
+ Provides-Extra: openai
37
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # memwal
41
+
42
+ Python SDK for [MemWal](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
43
+
44
+ All data processing (encryption, embedding, Walrus storage) happens server-side in a TEE. The SDK signs requests with your Ed25519 delegate key and sends text over HTTPS.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install memwal
50
+ ```
51
+
52
+ With optional integrations:
53
+
54
+ ```bash
55
+ pip install memwal[langchain] # LangChain support
56
+ pip install memwal[openai] # OpenAI SDK support
57
+ pip install memwal[all] # Everything
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ Set your environment variables first:
63
+
64
+ ```bash
65
+ export MEMWAL_KEY="your-ed25519-delegate-key-hex"
66
+ export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
67
+ export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
68
+ ```
69
+
70
+ ### Async (recommended)
71
+
72
+ ```python
73
+ import asyncio
74
+ import os
75
+ from memwal import MemWal
76
+
77
+ async def main():
78
+ memwal = MemWal.create(
79
+ key=os.environ["MEMWAL_KEY"],
80
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
81
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
82
+ )
83
+
84
+ # Store a memory
85
+ result = await memwal.remember("I'm allergic to peanuts")
86
+ print(result.blob_id)
87
+
88
+ # Recall memories
89
+ matches = await memwal.recall("food allergies")
90
+ for memory in matches.results:
91
+ print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
92
+
93
+ # Analyze conversation for facts
94
+ analysis = await memwal.analyze("I love coffee and live in Tokyo")
95
+ for fact in analysis.facts:
96
+ print(fact.text)
97
+
98
+ await memwal.close()
99
+
100
+ asyncio.run(main())
101
+ ```
102
+
103
+ ### Sync
104
+
105
+ ```python
106
+ import os
107
+ from memwal import MemWalSync
108
+
109
+ client = MemWalSync.create(
110
+ key=os.environ["MEMWAL_KEY"],
111
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
112
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
113
+ )
114
+
115
+ result = client.remember("I'm allergic to peanuts")
116
+ matches = client.recall("food allergies")
117
+ client.close()
118
+ ```
119
+
120
+ ### Context Manager
121
+
122
+ ```python
123
+ import os
124
+ from memwal import MemWal
125
+
126
+ async with MemWal.create(
127
+ key=os.environ["MEMWAL_KEY"],
128
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
129
+ ) as memwal:
130
+ await memwal.remember("I prefer dark mode")
131
+ ```
132
+
133
+ ## AI Middleware
134
+
135
+ ### LangChain
136
+
137
+ ```python
138
+ import os
139
+ from langchain_openai import ChatOpenAI
140
+ from langchain_core.messages import HumanMessage
141
+ from memwal import with_memwal_langchain
142
+
143
+ llm = ChatOpenAI(model="gpt-4o")
144
+ smart_llm = with_memwal_langchain(
145
+ llm,
146
+ key=os.environ["MEMWAL_KEY"],
147
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
148
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
149
+ max_memories=5,
150
+ min_relevance=0.3,
151
+ )
152
+
153
+ # Memories are automatically recalled and injected
154
+ response = await smart_llm.ainvoke([HumanMessage("What are my food allergies?")])
155
+ ```
156
+
157
+ ### OpenAI SDK
158
+
159
+ ```python
160
+ import os
161
+ from openai import AsyncOpenAI
162
+ from memwal import with_memwal_openai
163
+
164
+ client = AsyncOpenAI()
165
+ smart_client = with_memwal_openai(
166
+ client,
167
+ key=os.environ["MEMWAL_KEY"],
168
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
169
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
170
+ )
171
+
172
+ # Memories are automatically recalled and injected
173
+ response = await smart_client.chat.completions.create(
174
+ model="gpt-4o",
175
+ messages=[{"role": "user", "content": "What are my food allergies?"}],
176
+ )
177
+ ```
178
+
179
+ ## API Reference
180
+
181
+ ### `MemWal.create(key, account_id, server_url?, namespace?)`
182
+
183
+ Create a new async client.
184
+
185
+ ### Methods
186
+
187
+ | Method | Description |
188
+ |--------|-------------|
189
+ | `await remember(text, namespace?)` | Store a memory |
190
+ | `await recall(query, limit?, namespace?)` | Search memories |
191
+ | `await analyze(text, namespace?)` | Extract and store facts |
192
+ | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
193
+ | `await restore(namespace, limit?)` | Restore a namespace |
194
+ | `await health()` | Check server health |
195
+ | `await remember_manual(opts)` | Store with pre-computed vector |
196
+ | `await recall_manual(opts)` | Search with pre-computed vector |
197
+ | `await get_public_key_hex()` | Get Ed25519 public key |
198
+
199
+ ## Authentication
200
+
201
+ Every request is signed with Ed25519:
202
+
203
+ ```
204
+ message = f"{timestamp}.{method}.{path}.{sha256(body)}.{nonce}.{account_id}"
205
+ ```
206
+
207
+ Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, `x-delegate-key`, `x-account-id`.
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,172 @@
1
+ # memwal
2
+
3
+ Python SDK for [MemWal](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
4
+
5
+ All data processing (encryption, embedding, Walrus storage) happens server-side in a TEE. The SDK signs requests with your Ed25519 delegate key and sends text over HTTPS.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install memwal
11
+ ```
12
+
13
+ With optional integrations:
14
+
15
+ ```bash
16
+ pip install memwal[langchain] # LangChain support
17
+ pip install memwal[openai] # OpenAI SDK support
18
+ pip install memwal[all] # Everything
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ Set your environment variables first:
24
+
25
+ ```bash
26
+ export MEMWAL_KEY="your-ed25519-delegate-key-hex"
27
+ export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
28
+ export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
29
+ ```
30
+
31
+ ### Async (recommended)
32
+
33
+ ```python
34
+ import asyncio
35
+ import os
36
+ from memwal import MemWal
37
+
38
+ async def main():
39
+ memwal = MemWal.create(
40
+ key=os.environ["MEMWAL_KEY"],
41
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
42
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
43
+ )
44
+
45
+ # Store a memory
46
+ result = await memwal.remember("I'm allergic to peanuts")
47
+ print(result.blob_id)
48
+
49
+ # Recall memories
50
+ matches = await memwal.recall("food allergies")
51
+ for memory in matches.results:
52
+ print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
53
+
54
+ # Analyze conversation for facts
55
+ analysis = await memwal.analyze("I love coffee and live in Tokyo")
56
+ for fact in analysis.facts:
57
+ print(fact.text)
58
+
59
+ await memwal.close()
60
+
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ ### Sync
65
+
66
+ ```python
67
+ import os
68
+ from memwal import MemWalSync
69
+
70
+ client = MemWalSync.create(
71
+ key=os.environ["MEMWAL_KEY"],
72
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
73
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
74
+ )
75
+
76
+ result = client.remember("I'm allergic to peanuts")
77
+ matches = client.recall("food allergies")
78
+ client.close()
79
+ ```
80
+
81
+ ### Context Manager
82
+
83
+ ```python
84
+ import os
85
+ from memwal import MemWal
86
+
87
+ async with MemWal.create(
88
+ key=os.environ["MEMWAL_KEY"],
89
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
90
+ ) as memwal:
91
+ await memwal.remember("I prefer dark mode")
92
+ ```
93
+
94
+ ## AI Middleware
95
+
96
+ ### LangChain
97
+
98
+ ```python
99
+ import os
100
+ from langchain_openai import ChatOpenAI
101
+ from langchain_core.messages import HumanMessage
102
+ from memwal import with_memwal_langchain
103
+
104
+ llm = ChatOpenAI(model="gpt-4o")
105
+ smart_llm = with_memwal_langchain(
106
+ llm,
107
+ key=os.environ["MEMWAL_KEY"],
108
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
109
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
110
+ max_memories=5,
111
+ min_relevance=0.3,
112
+ )
113
+
114
+ # Memories are automatically recalled and injected
115
+ response = await smart_llm.ainvoke([HumanMessage("What are my food allergies?")])
116
+ ```
117
+
118
+ ### OpenAI SDK
119
+
120
+ ```python
121
+ import os
122
+ from openai import AsyncOpenAI
123
+ from memwal import with_memwal_openai
124
+
125
+ client = AsyncOpenAI()
126
+ smart_client = with_memwal_openai(
127
+ client,
128
+ key=os.environ["MEMWAL_KEY"],
129
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
130
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
131
+ )
132
+
133
+ # Memories are automatically recalled and injected
134
+ response = await smart_client.chat.completions.create(
135
+ model="gpt-4o",
136
+ messages=[{"role": "user", "content": "What are my food allergies?"}],
137
+ )
138
+ ```
139
+
140
+ ## API Reference
141
+
142
+ ### `MemWal.create(key, account_id, server_url?, namespace?)`
143
+
144
+ Create a new async client.
145
+
146
+ ### Methods
147
+
148
+ | Method | Description |
149
+ |--------|-------------|
150
+ | `await remember(text, namespace?)` | Store a memory |
151
+ | `await recall(query, limit?, namespace?)` | Search memories |
152
+ | `await analyze(text, namespace?)` | Extract and store facts |
153
+ | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
154
+ | `await restore(namespace, limit?)` | Restore a namespace |
155
+ | `await health()` | Check server health |
156
+ | `await remember_manual(opts)` | Store with pre-computed vector |
157
+ | `await recall_manual(opts)` | Search with pre-computed vector |
158
+ | `await get_public_key_hex()` | Get Ed25519 public key |
159
+
160
+ ## Authentication
161
+
162
+ Every request is signed with Ed25519:
163
+
164
+ ```
165
+ message = f"{timestamp}.{method}.{path}.{sha256(body)}.{nonce}.{account_id}"
166
+ ```
167
+
168
+ Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, `x-delegate-key`, `x-account-id`.
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,8 @@
1
+ # Local server (default) or remote relayer
2
+ MEMWAL_SERVER_URL=http://localhost:3001
3
+ # Ed25519 delegate private key (64-hex). Get from MemWal dashboard.
4
+ MEMWAL_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
5
+ # MemWalAccount object ID on Sui (the wallet's account)
6
+ MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
7
+ # Namespace for these test memories
8
+ MEMWAL_NAMESPACE=python-sdk-example
@@ -0,0 +1,2 @@
1
+ # Local credentials — never commit. Use .env.example as template.
2
+ .env
@@ -0,0 +1,177 @@
1
+ """End-to-end smoke test for the async remember family (PR #121 / ENG-1406+1408).
2
+
3
+ Loads credentials from ./.env (see ``.env.example``), then walks every new
4
+ method that was added to mirror the TypeScript SDK:
5
+
6
+ 1. health() sanity check
7
+ 2. remember() returns RememberAcceptedResult (~500ms)
8
+ 3. wait_for_remember_job() polls until ``done`` (~upload time)
9
+ 4. remember_and_wait() single call doing both
10
+ 5. recall() verify the new memory came back
11
+ 6. remember_bulk_and_wait() 3 facts in one call
12
+ 7. analyze_and_wait() LLM extracts facts + waits for them all
13
+ 8. embed() raw embedding vector
14
+
15
+ Run::
16
+
17
+ cd packages/python-sdk-memwal
18
+ cp examples/.env.example examples/.env # then fill in real values
19
+ python3 examples/async_remember_demo.py
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import sys
26
+ import time
27
+ from pathlib import Path
28
+
29
+
30
+ def _load_env() -> None:
31
+ """Tiny .env loader so the demo doesn't need python-dotenv."""
32
+
33
+ env_path = Path(__file__).parent / ".env"
34
+ if not env_path.exists():
35
+ return
36
+ for line in env_path.read_text().splitlines():
37
+ line = line.strip()
38
+ if not line or line.startswith("#") or "=" not in line:
39
+ continue
40
+ key, _, value = line.partition("=")
41
+ os.environ.setdefault(key.strip(), value.strip())
42
+
43
+
44
+ _load_env()
45
+
46
+ # Make ``import memwal`` work when running directly from the repo without
47
+ # `pip install -e .`.
48
+ sys.path.insert(0, str(Path(__file__).parent.parent))
49
+
50
+ from memwal import ( # noqa: E402
51
+ MemWal,
52
+ RememberBulkItem,
53
+ RememberBulkOptions,
54
+ )
55
+
56
+
57
+ def _section(title: str) -> None:
58
+ print(f"\n──────── {title} ────────")
59
+
60
+
61
+ def _ms(start: float) -> int:
62
+ return int((time.monotonic() - start) * 1000)
63
+
64
+
65
+ async def main() -> None:
66
+ server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:3001")
67
+ key = os.environ.get("MEMWAL_KEY")
68
+ account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
69
+ namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
70
+
71
+ if not key or not account_id:
72
+ print("ERROR: set MEMWAL_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
73
+ sys.exit(2)
74
+
75
+ print(f"server : {server_url}")
76
+ print(f"account_id : {account_id[:14]}...")
77
+ print(f"namespace : {namespace}")
78
+
79
+ async with MemWal.create(
80
+ key=key,
81
+ account_id=account_id,
82
+ server_url=server_url,
83
+ namespace=namespace,
84
+ ) as memwal:
85
+ # 1. health
86
+ _section("1. health()")
87
+ h = await memwal.health()
88
+ print(f" status={h.status} version={h.version}")
89
+
90
+ # 2. remember() → 202 + job_id (PR #121: should return in ~500ms)
91
+ _section("2. remember() — returns 202 + job_id")
92
+ t0 = time.monotonic()
93
+ accepted = await memwal.remember(
94
+ "Python SDK demo: I prefer FastAPI over Flask for async HTTP services."
95
+ )
96
+ accept_ms = _ms(t0)
97
+ print(f" accept_ms={accept_ms} job_id={accepted.job_id} status={accepted.status}")
98
+ assert accepted.job_id, "expected job_id"
99
+ assert accept_ms < 5000, f"expected <5s response, got {accept_ms}ms"
100
+
101
+ # 3. wait_for_remember_job → polls until "done"
102
+ _section("3. wait_for_remember_job(job_id)")
103
+ t1 = time.monotonic()
104
+ result = await memwal.wait_for_remember_job(accepted.job_id, timeout_ms=60_000)
105
+ wait_ms = _ms(t1)
106
+ print(f" wait_ms={wait_ms} blob_id={result.blob_id} ns={result.namespace}")
107
+
108
+ # 4. remember_and_wait — convenience
109
+ _section("4. remember_and_wait()")
110
+ t2 = time.monotonic()
111
+ full = await memwal.remember_and_wait(
112
+ "Python SDK demo: I drink black coffee in the morning.",
113
+ timeout_ms=60_000,
114
+ )
115
+ total_ms = _ms(t2)
116
+ print(f" total_ms={total_ms} blob_id={full.blob_id}")
117
+
118
+ # 5. recall — confirm the memories are searchable
119
+ _section("5. recall('coffee')")
120
+ rc = await memwal.recall("coffee", limit=3)
121
+ print(f" found {len(rc.results)} / total={rc.total}")
122
+ for m in rc.results[:3]:
123
+ print(f" [{m.distance:.3f}] {m.text[:80]}")
124
+
125
+ # 6. remember_bulk_and_wait — 3 items in one call
126
+ _section("6. remember_bulk_and_wait() — 3 items")
127
+ t3 = time.monotonic()
128
+ bulk = await memwal.remember_bulk_and_wait(
129
+ [
130
+ RememberBulkItem(text="Bulk demo 1: Trees clean the air."),
131
+ RememberBulkItem(text="Bulk demo 2: Coffee comes from beans."),
132
+ RememberBulkItem(text="Bulk demo 3: Mountains are usually cold."),
133
+ ],
134
+ opts=RememberBulkOptions(poll_interval_ms=2000, timeout_ms=120_000),
135
+ )
136
+ bulk_ms = _ms(t3)
137
+ print(
138
+ f" bulk_ms={bulk_ms} total={bulk.total} ok={bulk.succeeded} "
139
+ f"failed={bulk.failed} timed_out={bulk.timed_out}"
140
+ )
141
+ for r in bulk.results:
142
+ print(f" [{r.status}] id={r.id[:8]}... blob={r.blob_id[:20]}...")
143
+
144
+ # 7. analyze_and_wait — LLM splits text into facts + persists each
145
+ _section("7. analyze_and_wait()")
146
+ t4 = time.monotonic()
147
+ try:
148
+ an = await memwal.analyze_and_wait(
149
+ "Today I learned that the Pacific is the largest ocean and "
150
+ "that octopuses have three hearts.",
151
+ opts=RememberBulkOptions(timeout_ms=120_000),
152
+ )
153
+ ana_ms = _ms(t4)
154
+ print(
155
+ f" analyze_ms={ana_ms} facts={len(an.facts)} "
156
+ f"ok={an.succeeded} failed={an.failed} timed_out={an.timed_out}"
157
+ )
158
+ for f, r in zip(an.facts, an.results):
159
+ print(f" fact='{f.text[:60]}' status={r.status} blob={r.blob_id[:20]}...")
160
+ except Exception as e:
161
+ print(f" skipped (analyze requires server LLM key): {e}")
162
+
163
+ # 8. embed — raw embedding vector
164
+ _section("8. embed('hello world')")
165
+ try:
166
+ emb = await memwal.embed("hello world")
167
+ print(f" vector dims={len(emb.vector)} first 5={emb.vector[:5]}")
168
+ except Exception as e:
169
+ print(f" skipped (server may not expose /api/embed yet): {e}")
170
+
171
+ print("\n✅ all sections completed")
172
+
173
+
174
+ if __name__ == "__main__":
175
+ import asyncio
176
+
177
+ asyncio.run(main())