a2a-lite 0.2.1__py3-none-any.whl → 0.2.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.
- a2a_lite/__init__.py +7 -36
- a2a_lite/agent.py +31 -74
- a2a_lite/auth.py +2 -2
- a2a_lite/cli.py +2 -43
- a2a_lite/decorators.py +2 -3
- a2a_lite/executor.py +22 -23
- a2a_lite/streaming.py +0 -29
- a2a_lite/testing.py +10 -1
- a2a_lite/utils.py +5 -1
- {a2a_lite-0.2.1.dist-info → a2a_lite-0.2.3.dist-info}/METADATA +39 -92
- a2a_lite-0.2.3.dist-info/RECORD +16 -0
- a2a_lite/discovery.py +0 -152
- a2a_lite/human_loop.py +0 -284
- a2a_lite/webhooks.py +0 -232
- a2a_lite-0.2.1.dist-info/RECORD +0 -19
- {a2a_lite-0.2.1.dist-info → a2a_lite-0.2.3.dist-info}/WHEEL +0 -0
- {a2a_lite-0.2.1.dist-info → a2a_lite-0.2.3.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-lite
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Simplified wrapper for Google's A2A Protocol SDK
|
|
5
5
|
Author: A2A Lite Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,17 +21,17 @@ Requires-Dist: rich>=13.0
|
|
|
21
21
|
Requires-Dist: starlette>=0.40.0
|
|
22
22
|
Requires-Dist: typer>=0.9.0
|
|
23
23
|
Requires-Dist: uvicorn>=0.30.0
|
|
24
|
-
Requires-Dist: watchfiles>=0.20.0
|
|
25
|
-
Requires-Dist: zeroconf>=0.80.0
|
|
26
24
|
Provides-Extra: dev
|
|
27
25
|
Requires-Dist: httpx>=0.25; extra == 'dev'
|
|
28
26
|
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
29
27
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Provides-Extra: oauth
|
|
29
|
+
Requires-Dist: pyjwt[crypto]>=2.0; extra == 'oauth'
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
32
32
|
# A2A Lite - Python
|
|
33
33
|
|
|
34
|
-
**Build A2A agents in 8 lines. Add
|
|
34
|
+
**Build A2A agents in 8 lines. Add features when you need them.**
|
|
35
35
|
|
|
36
36
|
Wraps the official [A2A Python SDK](https://github.com/a2aproject/a2a-python) with a simple, intuitive API.
|
|
37
37
|
|
|
@@ -147,7 +147,6 @@ class User(BaseModel):
|
|
|
147
147
|
|
|
148
148
|
@agent.skill("create_user")
|
|
149
149
|
async def create_user(user: User) -> dict:
|
|
150
|
-
# 'user' is already a User instance — auto-converted from dict!
|
|
151
150
|
return {"id": 1, "name": user.name}
|
|
152
151
|
```
|
|
153
152
|
|
|
@@ -186,28 +185,13 @@ Built-in middleware:
|
|
|
186
185
|
```python
|
|
187
186
|
from a2a_lite import logging_middleware, timing_middleware, retry_middleware, rate_limit_middleware
|
|
188
187
|
|
|
189
|
-
agent.
|
|
190
|
-
agent.
|
|
191
|
-
agent.
|
|
192
|
-
agent.
|
|
188
|
+
agent.add_middleware(logging_middleware)
|
|
189
|
+
agent.add_middleware(timing_middleware)
|
|
190
|
+
agent.add_middleware(rate_limit_middleware(max_per_minute=60))
|
|
191
|
+
agent.add_middleware(retry_middleware(max_retries=3))
|
|
193
192
|
```
|
|
194
193
|
|
|
195
|
-
### Level 5:
|
|
196
|
-
|
|
197
|
-
```python
|
|
198
|
-
from a2a_lite import InteractionContext
|
|
199
|
-
|
|
200
|
-
@agent.skill("wizard")
|
|
201
|
-
async def wizard(ctx: InteractionContext) -> dict:
|
|
202
|
-
name = await ctx.ask("What's your name?")
|
|
203
|
-
role = await ctx.ask("Role?", options=["Dev", "Manager"])
|
|
204
|
-
|
|
205
|
-
if await ctx.confirm(f"Create {name} as {role}?"):
|
|
206
|
-
return {"created": name}
|
|
207
|
-
return {"cancelled": True}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Level 6: File Handling
|
|
194
|
+
### Level 5: File Handling
|
|
211
195
|
|
|
212
196
|
```python
|
|
213
197
|
from a2a_lite import FilePart
|
|
@@ -218,7 +202,7 @@ async def summarize(doc: FilePart) -> str:
|
|
|
218
202
|
return f"Summary: {content[:100]}..."
|
|
219
203
|
```
|
|
220
204
|
|
|
221
|
-
### Level
|
|
205
|
+
### Level 6: Task Tracking
|
|
222
206
|
|
|
223
207
|
```python
|
|
224
208
|
from a2a_lite import TaskContext
|
|
@@ -235,7 +219,7 @@ async def process(data: str, task: TaskContext) -> str:
|
|
|
235
219
|
return "Done!"
|
|
236
220
|
```
|
|
237
221
|
|
|
238
|
-
### Level
|
|
222
|
+
### Level 7: Authentication
|
|
239
223
|
|
|
240
224
|
```python
|
|
241
225
|
from a2a_lite import Agent, APIKeyAuth
|
|
@@ -260,14 +244,24 @@ agent = Agent(
|
|
|
260
244
|
auth=BearerAuth(secret="your-jwt-secret"),
|
|
261
245
|
)
|
|
262
246
|
|
|
263
|
-
# OAuth2
|
|
247
|
+
# OAuth2 (requires: pip install a2a-lite[oauth])
|
|
264
248
|
agent = Agent(
|
|
265
249
|
name="Bot", description="A bot",
|
|
266
250
|
auth=OAuth2Auth(issuer="https://auth.example.com", audience="my-api"),
|
|
267
251
|
)
|
|
268
252
|
```
|
|
269
253
|
|
|
270
|
-
|
|
254
|
+
Skills can receive auth results by type-hinting a parameter as `AuthResult`:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from a2a_lite.auth import AuthResult
|
|
258
|
+
|
|
259
|
+
@agent.skill("whoami")
|
|
260
|
+
async def whoami(auth: AuthResult) -> dict:
|
|
261
|
+
return {"user": auth.identity, "scheme": auth.scheme}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Level 8: CORS and Production Mode
|
|
271
265
|
|
|
272
266
|
```python
|
|
273
267
|
agent = Agent(
|
|
@@ -278,11 +272,11 @@ agent = Agent(
|
|
|
278
272
|
)
|
|
279
273
|
```
|
|
280
274
|
|
|
281
|
-
### Level
|
|
275
|
+
### Level 9: Completion Hooks
|
|
282
276
|
|
|
283
277
|
```python
|
|
284
278
|
@agent.on_complete
|
|
285
|
-
async def notify(skill_name, result):
|
|
279
|
+
async def notify(skill_name, result, ctx):
|
|
286
280
|
print(f"Skill {skill_name} completed with: {result}")
|
|
287
281
|
```
|
|
288
282
|
|
|
@@ -311,25 +305,16 @@ async def info(name: str, age: int) -> dict:
|
|
|
311
305
|
def test_simple_result():
|
|
312
306
|
client = AgentTestClient(agent)
|
|
313
307
|
result = client.call("greet", name="World")
|
|
314
|
-
# Simple values support direct equality
|
|
315
308
|
assert result == "Hello, World!"
|
|
316
309
|
|
|
317
310
|
|
|
318
311
|
def test_dict_result():
|
|
319
312
|
client = AgentTestClient(agent)
|
|
320
313
|
result = client.call("info", name="Alice", age=30)
|
|
321
|
-
# Access dict results via .data
|
|
322
314
|
assert result.data["name"] == "Alice"
|
|
323
315
|
assert result.data["age"] == 30
|
|
324
316
|
|
|
325
317
|
|
|
326
|
-
def test_text_access():
|
|
327
|
-
client = AgentTestClient(agent)
|
|
328
|
-
result = client.call("greet", name="World")
|
|
329
|
-
# .text gives the raw string
|
|
330
|
-
assert result.text == '"Hello, World!"'
|
|
331
|
-
|
|
332
|
-
|
|
333
318
|
def test_list_skills():
|
|
334
319
|
client = AgentTestClient(agent)
|
|
335
320
|
skills = client.list_skills()
|
|
@@ -377,59 +362,19 @@ def test_streaming():
|
|
|
377
362
|
|
|
378
363
|
---
|
|
379
364
|
|
|
380
|
-
## Task Store
|
|
381
|
-
|
|
382
|
-
The `TaskStore` provides async-safe task lifecycle management:
|
|
383
|
-
|
|
384
|
-
```python
|
|
385
|
-
from a2a_lite.tasks import TaskStore, TaskStatus
|
|
386
|
-
|
|
387
|
-
store = TaskStore()
|
|
388
|
-
|
|
389
|
-
# All operations are async and thread-safe
|
|
390
|
-
task = await store.create(task_id="task-1", skill="process")
|
|
391
|
-
task = await store.get("task-1")
|
|
392
|
-
await store.update("task-1", status=TaskStatus.WORKING, progress=0.5)
|
|
393
|
-
tasks = await store.list()
|
|
394
|
-
await store.delete("task-1")
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
---
|
|
398
|
-
|
|
399
|
-
## Agent Discovery
|
|
400
|
-
|
|
401
|
-
Find agents on your local network via mDNS:
|
|
402
|
-
|
|
403
|
-
```python
|
|
404
|
-
from a2a_lite import AgentDiscovery
|
|
405
|
-
|
|
406
|
-
# Advertise your agent
|
|
407
|
-
agent.run(port=8787, enable_discovery=True)
|
|
408
|
-
|
|
409
|
-
# Discover other agents
|
|
410
|
-
discovery = AgentDiscovery()
|
|
411
|
-
agents = await discovery.discover(timeout=5.0)
|
|
412
|
-
for a in agents:
|
|
413
|
-
print(f"{a.name} at {a.url}")
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
---
|
|
417
|
-
|
|
418
365
|
## CLI
|
|
419
366
|
|
|
420
367
|
```bash
|
|
421
368
|
a2a-lite init my-agent # Create new project
|
|
422
369
|
a2a-lite serve agent.py # Run agent from file
|
|
423
|
-
a2a-lite serve agent.py -r # Run with hot reload
|
|
424
370
|
a2a-lite inspect http://... # View agent capabilities
|
|
425
371
|
a2a-lite test http://... skill # Test a skill
|
|
426
|
-
a2a-lite discover # Find local agents
|
|
427
372
|
a2a-lite version # Show version
|
|
428
373
|
```
|
|
429
374
|
|
|
430
375
|
---
|
|
431
376
|
|
|
432
|
-
##
|
|
377
|
+
## API Reference
|
|
433
378
|
|
|
434
379
|
### Agent
|
|
435
380
|
|
|
@@ -443,7 +388,6 @@ Agent(
|
|
|
443
388
|
task_store: str | TaskStore = None, # "memory" or custom TaskStore
|
|
444
389
|
cors_origins: List[str] = None, # CORS allowed origins
|
|
445
390
|
production: bool = False, # Enable production warnings
|
|
446
|
-
enable_discovery: bool = False, # mDNS discovery
|
|
447
391
|
)
|
|
448
392
|
```
|
|
449
393
|
|
|
@@ -453,8 +397,11 @@ Agent(
|
|
|
453
397
|
|--------|-------------|
|
|
454
398
|
| `@agent.skill(name, **config)` | Register a skill via decorator |
|
|
455
399
|
| `@agent.middleware` | Register middleware via decorator |
|
|
456
|
-
| `agent.
|
|
400
|
+
| `agent.add_middleware(fn)` | Register middleware function |
|
|
457
401
|
| `@agent.on_complete` | Register completion hook |
|
|
402
|
+
| `@agent.on_startup` | Register startup hook |
|
|
403
|
+
| `@agent.on_shutdown` | Register shutdown hook |
|
|
404
|
+
| `@agent.on_error` | Register error handler |
|
|
458
405
|
| `agent.run(port=8787)` | Start the server |
|
|
459
406
|
| `agent.get_app()` | Get the ASGI app (for custom deployment) |
|
|
460
407
|
|
|
@@ -462,7 +409,7 @@ Agent(
|
|
|
462
409
|
|
|
463
410
|
```python
|
|
464
411
|
@agent.skill(
|
|
465
|
-
name: str,
|
|
412
|
+
name: str = None, # Skill name (defaults to function name)
|
|
466
413
|
description: str = None, # Human-readable description
|
|
467
414
|
tags: List[str] = None, # Categorization tags
|
|
468
415
|
streaming: bool = False, # Enable streaming
|
|
@@ -475,18 +422,19 @@ Agent(
|
|
|
475
422
|
|----------|-------|
|
|
476
423
|
| `APIKeyAuth(keys=[...])` | API key auth (keys hashed with SHA-256) |
|
|
477
424
|
| `BearerAuth(secret=...)` | JWT/Bearer token auth |
|
|
478
|
-
| `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth |
|
|
425
|
+
| `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth (requires `a2a-lite[oauth]`) |
|
|
479
426
|
| `NoAuth()` | No auth (default) |
|
|
480
427
|
|
|
481
428
|
### Special Parameter Types
|
|
482
429
|
|
|
483
|
-
These are auto-injected when detected in skill signatures:
|
|
430
|
+
These are auto-injected when detected in skill function signatures:
|
|
484
431
|
|
|
485
432
|
| Type | Description |
|
|
486
433
|
|------|-------------|
|
|
487
434
|
| `TaskContext` | Task lifecycle management (requires `task_store`) |
|
|
488
|
-
| `
|
|
435
|
+
| `AuthResult` | Authentication result injection |
|
|
489
436
|
| `FilePart` | File upload handling |
|
|
437
|
+
| `DataPart` | Structured data handling |
|
|
490
438
|
|
|
491
439
|
---
|
|
492
440
|
|
|
@@ -499,10 +447,9 @@ These are auto-injected when detected in skill signatures:
|
|
|
499
447
|
| [06_pydantic_models.py](examples/06_pydantic_models.py) | Auto Pydantic conversion |
|
|
500
448
|
| [08_streaming.py](examples/08_streaming.py) | Streaming responses |
|
|
501
449
|
| [09_testing.py](examples/09_testing.py) | Testing your agents |
|
|
502
|
-
| [
|
|
503
|
-
| [
|
|
504
|
-
| [
|
|
505
|
-
| [14_with_auth.py](examples/14_with_auth.py) | Authentication |
|
|
450
|
+
| [10_file_handling.py](examples/10_file_handling.py) | Handle files |
|
|
451
|
+
| [11_task_tracking.py](examples/11_task_tracking.py) | Progress updates |
|
|
452
|
+
| [12_with_auth.py](examples/12_with_auth.py) | Authentication |
|
|
506
453
|
|
|
507
454
|
---
|
|
508
455
|
|
|
@@ -514,9 +461,9 @@ A2A Lite wraps the official A2A Python SDK. Every feature maps to real A2A proto
|
|
|
514
461
|
|----------|--------------|
|
|
515
462
|
| `@agent.skill()` | Agent Skills |
|
|
516
463
|
| `streaming=True` | SSE Streaming |
|
|
517
|
-
| `InteractionContext.ask()` | `input-required` state |
|
|
518
464
|
| `TaskContext.update()` | Task lifecycle states |
|
|
519
465
|
| `FilePart` | A2A File parts |
|
|
466
|
+
| `DataPart` | A2A Data parts |
|
|
520
467
|
| `APIKeyAuth` / `BearerAuth` | Security schemes |
|
|
521
468
|
|
|
522
469
|
---
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
a2a_lite/__init__.py,sha256=92WTqzqJV2FTBV0peiuLUdB9y5BZe37jvCT7E2k_vNQ,2642
|
|
2
|
+
a2a_lite/agent.py,sha256=2x4t4ysh10Qwjwp4S8pQehf8O3sINGoJIvq9YN9G-q0,15321
|
|
3
|
+
a2a_lite/auth.py,sha256=A1AMncM0cuWEAcysjumAhjd0lI_jMLmJpeYWuAPj30A,10181
|
|
4
|
+
a2a_lite/cli.py,sha256=5oAwZLR59ebxhuJjIYSHd_AskZqToQLRz45jAAlEBp0,7680
|
|
5
|
+
a2a_lite/decorators.py,sha256=rRzpeVW6mZTAFpNzZYDdraShbDdq7S32c4Z16edD4xw,979
|
|
6
|
+
a2a_lite/executor.py,sha256=pZx_3_JV34qWgbNDkzCWmT1qJRuBAxhrmz4luU7bDBc,13163
|
|
7
|
+
a2a_lite/middleware.py,sha256=c6jb9aFfyTf-JY6KjqaSgFJmpzqbHLC6Q1h9NNteqzo,5545
|
|
8
|
+
a2a_lite/parts.py,sha256=qVRiD-H9_NlMPk-R0gTUiGVQ77E2poiuBWAUyAyAoTI,6177
|
|
9
|
+
a2a_lite/streaming.py,sha256=6qLMUlbk61HBhtotxhSz5Yh4wL9OHeGt24o_J--RJvI,1626
|
|
10
|
+
a2a_lite/tasks.py,sha256=UpmDP-VGIQ1LodBNq4zx2pJElQ31gOJOAduHFBVyxOA,7039
|
|
11
|
+
a2a_lite/testing.py,sha256=M9IbLA6oUz1DokJ9Sc_r0gK43NNkU78IVkiBRuDFFCU,9393
|
|
12
|
+
a2a_lite/utils.py,sha256=AFLYQ4J-F7H_HeYWAeg8H3p9EOdDv4dOpju_ebrU5PI,3934
|
|
13
|
+
a2a_lite-0.2.3.dist-info/METADATA,sha256=x9DLlTzprZMk8qT210WM4dMxHiDaWjX5h9GFnbVxGrY,11392
|
|
14
|
+
a2a_lite-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
a2a_lite-0.2.3.dist-info/entry_points.txt,sha256=BONfFqZbCntNal2iwlTJAE09gCUvurfvqslMYVYh4is,46
|
|
16
|
+
a2a_lite-0.2.3.dist-info/RECORD,,
|
a2a_lite/discovery.py
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
mDNS-based local agent discovery using Zeroconf.
|
|
3
|
-
"""
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import asyncio
|
|
7
|
-
import logging
|
|
8
|
-
import socket
|
|
9
|
-
from typing import Dict, List, Optional
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf, ServiceInfo
|
|
15
|
-
from zeroconf.asyncio import AsyncZeroconf, AsyncServiceBrowser
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
SERVICE_TYPE = "_a2a-agent._tcp.local."
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass
|
|
22
|
-
class DiscoveredAgent:
|
|
23
|
-
"""Information about a discovered A2A agent."""
|
|
24
|
-
name: str
|
|
25
|
-
host: str
|
|
26
|
-
port: int
|
|
27
|
-
url: str
|
|
28
|
-
properties: Dict[str, str]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class AgentDiscovery:
|
|
32
|
-
"""
|
|
33
|
-
Discover A2A agents on the local network using mDNS.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
def __init__(self):
|
|
37
|
-
self._discovered: Dict[str, DiscoveredAgent] = {}
|
|
38
|
-
self._zeroconf: Optional[Zeroconf] = None
|
|
39
|
-
self._service_info: Optional[ServiceInfo] = None
|
|
40
|
-
|
|
41
|
-
async def discover(self, timeout: float = 5.0) -> List[DiscoveredAgent]:
|
|
42
|
-
"""
|
|
43
|
-
Discover agents on the local network.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
timeout: How long to wait for discoveries
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
List of discovered agents
|
|
50
|
-
"""
|
|
51
|
-
self._discovered.clear()
|
|
52
|
-
|
|
53
|
-
async with AsyncZeroconf() as azc:
|
|
54
|
-
listener = _DiscoveryListener(self._discovered)
|
|
55
|
-
browser = AsyncServiceBrowser(
|
|
56
|
-
azc.zeroconf, SERVICE_TYPE, listener
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
await asyncio.sleep(timeout)
|
|
60
|
-
await browser.async_cancel()
|
|
61
|
-
|
|
62
|
-
return list(self._discovered.values())
|
|
63
|
-
|
|
64
|
-
def register(
|
|
65
|
-
self,
|
|
66
|
-
name: str,
|
|
67
|
-
port: int,
|
|
68
|
-
properties: Optional[Dict[str, str]] = None,
|
|
69
|
-
) -> ServiceInfo:
|
|
70
|
-
"""
|
|
71
|
-
Register this agent for discovery.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
name: Agent name
|
|
75
|
-
port: Port the agent is running on
|
|
76
|
-
properties: Additional properties to advertise
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
ServiceInfo for unregistration
|
|
80
|
-
"""
|
|
81
|
-
hostname = socket.gethostname()
|
|
82
|
-
local_ip = self._get_local_ip()
|
|
83
|
-
|
|
84
|
-
# Sanitize name for mDNS (remove spaces and special chars)
|
|
85
|
-
safe_name = "".join(c if c.isalnum() or c == "-" else "-" for c in name)
|
|
86
|
-
|
|
87
|
-
info = ServiceInfo(
|
|
88
|
-
SERVICE_TYPE,
|
|
89
|
-
f"{safe_name}.{SERVICE_TYPE}",
|
|
90
|
-
addresses=[socket.inet_aton(local_ip)],
|
|
91
|
-
port=port,
|
|
92
|
-
properties=properties or {},
|
|
93
|
-
server=f"{hostname}.local.",
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
self._zeroconf = Zeroconf()
|
|
97
|
-
self._zeroconf.register_service(info)
|
|
98
|
-
self._service_info = info
|
|
99
|
-
|
|
100
|
-
return info
|
|
101
|
-
|
|
102
|
-
def unregister(self) -> None:
|
|
103
|
-
"""Unregister this agent from discovery."""
|
|
104
|
-
if self._zeroconf and self._service_info:
|
|
105
|
-
self._zeroconf.unregister_service(self._service_info)
|
|
106
|
-
self._zeroconf.close()
|
|
107
|
-
self._zeroconf = None
|
|
108
|
-
self._service_info = None
|
|
109
|
-
|
|
110
|
-
def _get_local_ip(self) -> str:
|
|
111
|
-
"""Get local IP address."""
|
|
112
|
-
try:
|
|
113
|
-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
114
|
-
s.connect(("8.8.8.8", 80))
|
|
115
|
-
ip = s.getsockname()[0]
|
|
116
|
-
s.close()
|
|
117
|
-
return ip
|
|
118
|
-
except Exception:
|
|
119
|
-
logger.debug("Could not detect local IP, falling back to 127.0.0.1")
|
|
120
|
-
return "127.0.0.1"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class _DiscoveryListener(ServiceListener):
|
|
124
|
-
"""Internal listener for mDNS service discovery."""
|
|
125
|
-
|
|
126
|
-
def __init__(self, discovered: Dict[str, DiscoveredAgent]):
|
|
127
|
-
self.discovered = discovered
|
|
128
|
-
|
|
129
|
-
def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
|
130
|
-
info = zc.get_service_info(type_, name)
|
|
131
|
-
if info:
|
|
132
|
-
agent_name = name.replace(f".{SERVICE_TYPE}", "")
|
|
133
|
-
host = socket.inet_ntoa(info.addresses[0]) if info.addresses else "localhost"
|
|
134
|
-
port = info.port
|
|
135
|
-
|
|
136
|
-
self.discovered[name] = DiscoveredAgent(
|
|
137
|
-
name=agent_name,
|
|
138
|
-
host=host,
|
|
139
|
-
port=port,
|
|
140
|
-
url=f"http://{host}:{port}",
|
|
141
|
-
properties={
|
|
142
|
-
k.decode() if isinstance(k, bytes) else k:
|
|
143
|
-
v.decode() if isinstance(v, bytes) else str(v)
|
|
144
|
-
for k, v in info.properties.items()
|
|
145
|
-
},
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
|
149
|
-
self.discovered.pop(name, None)
|
|
150
|
-
|
|
151
|
-
def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
|
152
|
-
self.add_service(zc, type_, name)
|