nowy-sdk 0.1.0__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,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: nowy-sdk
3
+ Version: 0.1.0
4
+ Summary: Nowy protocol WebSocket client SDK with generated capability types
5
+ Author: Nowy contributors
6
+ License-Expression: MIT
7
+ Keywords: nowy,websocket,agent,sdk
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: websockets>=13.0
17
+ Requires-Dist: python-ulid>=3.0.0
18
+ Requires-Dist: httpx>=0.28.0
19
+
20
+ # nowy-sdk
21
+
22
+ Python client for the [Nowy](https://github.com/) protocol — WebSocket Node Agent, Supabase auth helpers, and generated capability types.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install nowy-sdk
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ```python
33
+ import asyncio
34
+ from nowy_sdk import NowyClient
35
+ from nowy_sdk.auth import resolve_auth_token
36
+
37
+ async def main():
38
+ token = resolve_auth_token() # NOWY_EMAIL/PASSWORD in .env
39
+ client = NowyClient()
40
+ await client.connect(auth_token=token)
41
+ req = await client.send_text("hola nowy")
42
+ await client.wait_for_request(req)
43
+ print(client.last_response_text)
44
+ await client.close()
45
+
46
+ asyncio.run(main())
47
+ ```
48
+
49
+ ## Types
50
+
51
+ Generated from `specs/capabilities/*.yaml`:
52
+
53
+ ```python
54
+ from nowy_sdk import ReminderCreateInput, CAPABILITY_IDS
55
+ ```
56
+
57
+ Regenerate locally: `python scripts/codegen_capabilities.py` (from repo root).
@@ -0,0 +1,38 @@
1
+ # nowy-sdk
2
+
3
+ Python client for the [Nowy](https://github.com/) protocol — WebSocket Node Agent, Supabase auth helpers, and generated capability types.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install nowy-sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ import asyncio
15
+ from nowy_sdk import NowyClient
16
+ from nowy_sdk.auth import resolve_auth_token
17
+
18
+ async def main():
19
+ token = resolve_auth_token() # NOWY_EMAIL/PASSWORD in .env
20
+ client = NowyClient()
21
+ await client.connect(auth_token=token)
22
+ req = await client.send_text("hola nowy")
23
+ await client.wait_for_request(req)
24
+ print(client.last_response_text)
25
+ await client.close()
26
+
27
+ asyncio.run(main())
28
+ ```
29
+
30
+ ## Types
31
+
32
+ Generated from `specs/capabilities/*.yaml`:
33
+
34
+ ```python
35
+ from nowy_sdk import ReminderCreateInput, CAPABILITY_IDS
36
+ ```
37
+
38
+ Regenerate locally: `python scripts/codegen_capabilities.py` (from repo root).
@@ -0,0 +1,34 @@
1
+ """Nowy Python SDK — Phase 2."""
2
+
3
+ from nowy_sdk.client import NowyClient
4
+ from nowy_sdk.generated.capabilities import (
5
+ CAPABILITY_IDS,
6
+ CalendarTodayInput,
7
+ CalendarTodayOutput,
8
+ ReminderCreateInput,
9
+ ReminderCreateOutput,
10
+ ReminderListInput,
11
+ ReminderListOutput,
12
+ SearchDocumentInput,
13
+ SearchDocumentOutput,
14
+ )
15
+ from nowy_sdk.wire import DEFAULT_PROTOCOL_VERSION, DEFAULT_WS_PATH, new_request_id, new_runtime_id, wire_message
16
+
17
+ __version__ = "0.1.0"
18
+ __all__ = [
19
+ "CAPABILITY_IDS",
20
+ "CalendarTodayInput",
21
+ "CalendarTodayOutput",
22
+ "DEFAULT_PROTOCOL_VERSION",
23
+ "DEFAULT_WS_PATH",
24
+ "NowyClient",
25
+ "ReminderCreateInput",
26
+ "ReminderCreateOutput",
27
+ "ReminderListInput",
28
+ "ReminderListOutput",
29
+ "SearchDocumentInput",
30
+ "SearchDocumentOutput",
31
+ "new_request_id",
32
+ "new_runtime_id",
33
+ "wire_message",
34
+ ]
@@ -0,0 +1,95 @@
1
+ """Resolve a Supabase JWT for SDK smoke tests and scripts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+
11
+ REPO_ROOT = Path(__file__).resolve().parents[3]
12
+
13
+
14
+ def _parse_dotenv(path: Path) -> dict[str, str]:
15
+ if not path.is_file():
16
+ return {}
17
+ values: dict[str, str] = {}
18
+ for line in path.read_text(encoding="utf-8").splitlines():
19
+ stripped = line.strip()
20
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
21
+ continue
22
+ key, value = stripped.split("=", 1)
23
+ values[key.strip()] = value.strip().strip('"').strip("'")
24
+ return values
25
+
26
+
27
+ def _load_supabase_config() -> tuple[str, str]:
28
+ client_url = ""
29
+ client_key = ""
30
+ client_json = REPO_ROOT / "clients" / "nowy_app" / ".env.client.json"
31
+ if client_json.is_file():
32
+ data = json.loads(client_json.read_text(encoding="utf-8"))
33
+ client_url = str(data.get("SUPABASE_URL", "")).strip()
34
+ client_key = str(data.get("SUPABASE_ANON_KEY", "")).strip()
35
+
36
+ root_env = _parse_dotenv(REPO_ROOT / ".env")
37
+ url = (
38
+ os.environ.get("SUPABASE_URL", "").strip()
39
+ or root_env.get("SUPABASE_URL", "")
40
+ or client_url
41
+ )
42
+ # Client login must use the same publishable key as Flutter (.env.client.json).
43
+ anon_key = (
44
+ client_key
45
+ or os.environ.get("SUPABASE_ANON_KEY", "").strip()
46
+ or root_env.get("SUPABASE_ANON_KEY", "")
47
+ )
48
+ return url, anon_key
49
+
50
+
51
+ def _login_with_password(supabase_url: str, anon_key: str, email: str, password: str) -> str:
52
+ endpoint = f"{supabase_url.rstrip('/')}/auth/v1/token?grant_type=password"
53
+ headers = {
54
+ "apikey": anon_key,
55
+ "Content-Type": "application/json",
56
+ }
57
+ # Legacy JWT anon keys may be mirrored in Authorization; publishable keys must not.
58
+ if anon_key.startswith("eyJ"):
59
+ headers["Authorization"] = f"Bearer {anon_key}"
60
+ response = httpx.post(
61
+ endpoint,
62
+ headers=headers,
63
+ json={"email": email, "password": password},
64
+ timeout=15.0,
65
+ )
66
+ if response.status_code >= 400:
67
+ detail = response.text
68
+ try:
69
+ detail = response.json().get("error_description") or response.json().get("msg") or detail
70
+ except Exception: # noqa: BLE001
71
+ pass
72
+ raise RuntimeError(f"Supabase login failed ({response.status_code}): {detail}")
73
+ token = response.json().get("access_token")
74
+ if not token:
75
+ raise RuntimeError("Supabase login OK but access_token missing")
76
+ return str(token)
77
+
78
+
79
+ def resolve_auth_token() -> str | None:
80
+ """Return JWT from env, or sign in with NOWY_EMAIL + NOWY_PASSWORD."""
81
+ direct = os.environ.get("NOWY_AUTH_TOKEN", "").strip()
82
+ if direct:
83
+ return direct
84
+
85
+ root_env = _parse_dotenv(REPO_ROOT / ".env")
86
+ email = os.environ.get("NOWY_EMAIL", root_env.get("NOWY_EMAIL", "")).strip()
87
+ password = os.environ.get("NOWY_PASSWORD", root_env.get("NOWY_PASSWORD", "")).strip()
88
+ if not email or not password:
89
+ return None
90
+
91
+ supabase_url, anon_key = _load_supabase_config()
92
+ if not supabase_url or not anon_key:
93
+ raise RuntimeError("SUPABASE_URL y SUPABASE_ANON_KEY requeridos para login automatico")
94
+
95
+ return _login_with_password(supabase_url, anon_key, email, password)
@@ -0,0 +1,213 @@
1
+ """Minimal async WebSocket client for the Nowy runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from collections.abc import Awaitable, Callable
8
+ from typing import Any
9
+
10
+ import websockets
11
+ from websockets.asyncio.client import ClientConnection
12
+
13
+ from nowy_sdk.wire import DEFAULT_PROTOCOL_VERSION, new_request_id, new_runtime_id, wire_message
14
+
15
+ MessageHandler = Callable[[dict[str, Any]], Awaitable[None] | None]
16
+
17
+
18
+ class NowyClient:
19
+ def __init__(
20
+ self,
21
+ *,
22
+ url: str = "ws://127.0.0.1:8000/nowy/v1/connect",
23
+ client_name: str = "nowy-sdk-python",
24
+ client_version: str = "0.1.0",
25
+ platform: str = "web",
26
+ ) -> None:
27
+ self.url = url
28
+ self.client_name = client_name
29
+ self.client_version = client_version
30
+ self.platform = platform
31
+ self._ws: ClientConnection | None = None
32
+ self._recv_task: asyncio.Task[None] | None = None
33
+ self._handlers: list[MessageHandler] = []
34
+ self.runtime_id = new_runtime_id()
35
+ self.session_id: str | None = None
36
+ self.protocol_version = DEFAULT_PROTOCOL_VERSION
37
+ self.heartbeat_interval_ms = 30_000
38
+ self._heartbeat_task: asyncio.Task[None] | None = None
39
+ self._ready = asyncio.Event()
40
+ self.last_response_text = ""
41
+ self.last_notification: dict[str, Any] | None = None
42
+
43
+ def on_message(self, handler: MessageHandler) -> None:
44
+ self._handlers.append(handler)
45
+
46
+ async def connect(self, *, auth_token: str | None = None) -> None:
47
+ await self.close()
48
+ self._ws = await websockets.connect(self.url)
49
+ self._recv_task = asyncio.create_task(self._recv_loop())
50
+ await self._send(
51
+ wire_message(
52
+ type="protocol.hello",
53
+ payload={
54
+ "protocolVersion": DEFAULT_PROTOCOL_VERSION,
55
+ "supportedVersions": [DEFAULT_PROTOCOL_VERSION],
56
+ "client": {
57
+ "name": self.client_name,
58
+ "version": self.client_version,
59
+ "platform": self.platform,
60
+ },
61
+ },
62
+ )
63
+ )
64
+ welcome = await self._wait_for("protocol.welcome")
65
+ payload = welcome.get("payload", {})
66
+ if payload.get("accepted") is False:
67
+ raise ConnectionError(f"protocol rejected: {payload.get('reason')}")
68
+ self.protocol_version = int(payload.get("negotiatedVersion", DEFAULT_PROTOCOL_VERSION))
69
+
70
+ reg_request_id = new_request_id()
71
+ register_payload: dict[str, Any] = {
72
+ "platform": self.platform,
73
+ "nodeType": self.platform,
74
+ "locale": "es",
75
+ "declaredHardware": ["display"],
76
+ }
77
+ if auth_token:
78
+ register_payload["auth"] = {"token": auth_token}
79
+ await self._send(
80
+ wire_message(
81
+ type="runtime.register",
82
+ protocol_version=self.protocol_version,
83
+ runtime_id=self.runtime_id,
84
+ request_id=reg_request_id,
85
+ correlation_id=reg_request_id,
86
+ payload=register_payload,
87
+ )
88
+ )
89
+ registered = await self._wait_for("runtime.registered", request_id=reg_request_id)
90
+ reg_payload = registered.get("payload", {})
91
+ self.session_id = reg_payload.get("sessionId")
92
+ self.heartbeat_interval_ms = int(reg_payload.get("heartbeatIntervalMs", 30_000))
93
+ self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
94
+ self._ready.set()
95
+
96
+ async def send_text(self, text: str, *, locale: str = "es") -> str:
97
+ await self._ready.wait()
98
+ if not self.session_id:
99
+ raise RuntimeError("not registered")
100
+ request_id = new_request_id()
101
+ await self._send(
102
+ wire_message(
103
+ type="context.input.text",
104
+ protocol_version=self.protocol_version,
105
+ runtime_id=self.runtime_id,
106
+ session_id=self.session_id,
107
+ request_id=request_id,
108
+ correlation_id=request_id,
109
+ payload={"text": text.strip(), "locale": locale},
110
+ )
111
+ )
112
+ return request_id
113
+
114
+ async def wait_for_request(self, request_id: str, *, timeout: float = 60.0) -> dict[str, Any]:
115
+ future: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()
116
+
117
+ async def on_message(msg: dict[str, Any]) -> None:
118
+ if msg.get("requestId") != request_id:
119
+ return
120
+ if msg.get("type") == "request.completed" and not future.done():
121
+ future.set_result(msg)
122
+
123
+ self.on_message(on_message)
124
+ return await asyncio.wait_for(future, timeout=timeout)
125
+
126
+ async def close(self) -> None:
127
+ self._ready.clear()
128
+ if self._heartbeat_task:
129
+ self._heartbeat_task.cancel()
130
+ self._heartbeat_task = None
131
+ if self._recv_task:
132
+ self._recv_task.cancel()
133
+ self._recv_task = None
134
+ if self._ws:
135
+ await self._ws.close()
136
+ self._ws = None
137
+ self.session_id = None
138
+
139
+ async def _send(self, message: dict[str, Any]) -> None:
140
+ if not self._ws:
141
+ raise RuntimeError("not connected")
142
+ await self._ws.send(json.dumps(message))
143
+
144
+ async def _recv_loop(self) -> None:
145
+ assert self._ws is not None
146
+ try:
147
+ async for raw in self._ws:
148
+ msg = json.loads(raw)
149
+ self._track_wire_state(msg)
150
+ for handler in self._handlers:
151
+ result = handler(msg)
152
+ if asyncio.iscoroutine(result):
153
+ await result
154
+ except asyncio.CancelledError:
155
+ return
156
+
157
+ def _track_wire_state(self, msg: dict[str, Any]) -> None:
158
+ msg_type = msg.get("type", "")
159
+ payload = msg.get("payload", {})
160
+ if msg_type in ("response.partial", "response.completed"):
161
+ text = payload.get("text")
162
+ if isinstance(text, str):
163
+ self.last_response_text = text
164
+ if msg_type == "notification.reminder":
165
+ self.last_notification = payload if isinstance(payload, dict) else None
166
+
167
+ async def _heartbeat_loop(self) -> None:
168
+ interval = max(self.heartbeat_interval_ms, 5_000) / 1000
169
+ while True:
170
+ await asyncio.sleep(interval)
171
+ if not self.session_id:
172
+ continue
173
+ await self._send(
174
+ wire_message(
175
+ type="runtime.heartbeat",
176
+ protocol_version=self.protocol_version,
177
+ runtime_id=self.runtime_id,
178
+ session_id=self.session_id,
179
+ payload={},
180
+ )
181
+ )
182
+
183
+ async def _wait_for(
184
+ self,
185
+ message_type: str,
186
+ *,
187
+ request_id: str | None = None,
188
+ timeout: float = 10.0,
189
+ ) -> dict[str, Any]:
190
+ future: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()
191
+
192
+ async def on_message(msg: dict[str, Any]) -> None:
193
+ if request_id is not None and msg.get("requestId") != request_id:
194
+ return
195
+ if msg.get("type") == "protocol.error" and not future.done():
196
+ payload = msg.get("payload", {})
197
+ code = payload.get("code", "PROTOCOL_ERROR")
198
+ message = payload.get("message", "protocol error")
199
+ future.set_exception(ConnectionError(f"{code}: {message}"))
200
+ return
201
+ if msg.get("type") != message_type:
202
+ return
203
+ if not future.done():
204
+ future.set_result(msg)
205
+
206
+ self.on_message(on_message)
207
+ try:
208
+ return await asyncio.wait_for(future, timeout=timeout)
209
+ except TimeoutError as exc:
210
+ hint = ""
211
+ if message_type == "runtime.registered":
212
+ hint = " (¿auth requerido? exporta NOWY_AUTH_TOKEN con un JWT de Supabase)"
213
+ raise TimeoutError(f"timed out waiting for {message_type}{hint}") from exc
@@ -0,0 +1 @@
1
+ """Generated capability types."""
@@ -0,0 +1,57 @@
1
+ """AUTO-GENERATED — do not edit. Run: python scripts/codegen_capabilities.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypedDict
6
+
7
+ class CalendarTodayEventsItem(TypedDict, total=False):
8
+ title: str
9
+ start: str
10
+ end: str
11
+
12
+
13
+ class ReminderListRemindersItem(TypedDict, total=False):
14
+ id: str
15
+ text: str
16
+ fire_at: str
17
+ status: str
18
+ created_at: str
19
+
20
+
21
+ class SearchDocumentDocumentsItem(TypedDict, total=False):
22
+ id: str
23
+ title: str
24
+ snippet: str
25
+
26
+
27
+ class CalendarTodayInput(TypedDict, total=False):
28
+ timezone: str
29
+
30
+ class CalendarTodayOutput(TypedDict, total=False):
31
+ events: list[CalendarTodayEventsItem]
32
+
33
+ class ReminderCreateInput(TypedDict, total=False):
34
+ text: str
35
+ datetime: str
36
+
37
+ class ReminderCreateOutput(TypedDict, total=False):
38
+ reminderId: str
39
+
40
+ class ReminderListInput(TypedDict, total=False):
41
+ status: str
42
+
43
+ class ReminderListOutput(TypedDict, total=False):
44
+ reminders: list[ReminderListRemindersItem]
45
+
46
+ class SearchDocumentInput(TypedDict, total=False):
47
+ query: str
48
+
49
+ class SearchDocumentOutput(TypedDict, total=False):
50
+ documents: list[SearchDocumentDocumentsItem]
51
+
52
+ CAPABILITY_IDS: tuple[str, ...] = (
53
+ "calendar.today",
54
+ "reminder.create",
55
+ "reminder.list",
56
+ "search.document",
57
+ )
@@ -0,0 +1,49 @@
1
+ """Wire message helpers for the Nowy protocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import UTC, datetime
7
+ from typing import Any
8
+
9
+ from ulid import ULID
10
+
11
+ DEFAULT_PROTOCOL_VERSION = 1
12
+ DEFAULT_WS_PATH = "/nowy/v1/connect"
13
+
14
+
15
+ def new_runtime_id() -> str:
16
+ return f"rt_{ULID()}"
17
+
18
+
19
+ def new_request_id() -> str:
20
+ return f"req_{ULID()}"
21
+
22
+
23
+ def wire_message(
24
+ *,
25
+ type: str,
26
+ payload: dict[str, Any],
27
+ protocol_version: int = DEFAULT_PROTOCOL_VERSION,
28
+ runtime_id: str | None = None,
29
+ session_id: str | None = None,
30
+ request_id: str | None = None,
31
+ correlation_id: str | None = None,
32
+ ) -> dict[str, Any]:
33
+ msg: dict[str, Any] = {
34
+ "messageId": str(uuid.uuid4()),
35
+ "type": type,
36
+ "timestamp": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
37
+ "payload": payload,
38
+ }
39
+ if type != "protocol.hello":
40
+ msg["protocolVersion"] = protocol_version
41
+ if runtime_id:
42
+ msg["runtimeId"] = runtime_id
43
+ if session_id:
44
+ msg["sessionId"] = session_id
45
+ if request_id:
46
+ msg["requestId"] = request_id
47
+ if correlation_id:
48
+ msg["correlationId"] = correlation_id
49
+ return msg
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: nowy-sdk
3
+ Version: 0.1.0
4
+ Summary: Nowy protocol WebSocket client SDK with generated capability types
5
+ Author: Nowy contributors
6
+ License-Expression: MIT
7
+ Keywords: nowy,websocket,agent,sdk
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: websockets>=13.0
17
+ Requires-Dist: python-ulid>=3.0.0
18
+ Requires-Dist: httpx>=0.28.0
19
+
20
+ # nowy-sdk
21
+
22
+ Python client for the [Nowy](https://github.com/) protocol — WebSocket Node Agent, Supabase auth helpers, and generated capability types.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install nowy-sdk
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ```python
33
+ import asyncio
34
+ from nowy_sdk import NowyClient
35
+ from nowy_sdk.auth import resolve_auth_token
36
+
37
+ async def main():
38
+ token = resolve_auth_token() # NOWY_EMAIL/PASSWORD in .env
39
+ client = NowyClient()
40
+ await client.connect(auth_token=token)
41
+ req = await client.send_text("hola nowy")
42
+ await client.wait_for_request(req)
43
+ print(client.last_response_text)
44
+ await client.close()
45
+
46
+ asyncio.run(main())
47
+ ```
48
+
49
+ ## Types
50
+
51
+ Generated from `specs/capabilities/*.yaml`:
52
+
53
+ ```python
54
+ from nowy_sdk import ReminderCreateInput, CAPABILITY_IDS
55
+ ```
56
+
57
+ Regenerate locally: `python scripts/codegen_capabilities.py` (from repo root).
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ nowy_sdk/__init__.py
4
+ nowy_sdk/auth.py
5
+ nowy_sdk/client.py
6
+ nowy_sdk/wire.py
7
+ nowy_sdk.egg-info/PKG-INFO
8
+ nowy_sdk.egg-info/SOURCES.txt
9
+ nowy_sdk.egg-info/dependency_links.txt
10
+ nowy_sdk.egg-info/requires.txt
11
+ nowy_sdk.egg-info/top_level.txt
12
+ nowy_sdk/generated/__init__.py
13
+ nowy_sdk/generated/capabilities.py
@@ -0,0 +1,3 @@
1
+ websockets>=13.0
2
+ python-ulid>=3.0.0
3
+ httpx>=0.28.0
@@ -0,0 +1 @@
1
+ nowy_sdk
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nowy-sdk"
7
+ version = "0.1.0"
8
+ description = "Nowy protocol WebSocket client SDK with generated capability types"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Nowy contributors" }]
13
+ keywords = ["nowy", "websocket", "agent", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "websockets>=13.0",
24
+ "python-ulid>=3.0.0",
25
+ "httpx>=0.28.0",
26
+ ]
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["."]
30
+ include = ["nowy_sdk*"]
31
+
32
+ [tool.setuptools.package-data]
33
+ nowy_sdk = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+