unrealon 0.1.18__tar.gz → 0.1.20__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.
- {unrealon-0.1.18 → unrealon-0.1.20}/PKG-INFO +5 -1
- unrealon-0.1.20/github/README.md +230 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/github/pyproject.toml +1 -1
- {unrealon-0.1.18 → unrealon-0.1.20}/pyproject.toml +11 -1
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/__init__.py +5 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/__init__.py +2 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/enums.py +16 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/models.py +11 -18
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_client.py +18 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_version.py +1 -1
- unrealon-0.1.20/src/unrealon/parsers/README.md +400 -0
- unrealon-0.1.20/src/unrealon/parsers/__init__.py +51 -0
- unrealon-0.1.20/src/unrealon/parsers/api_parser.py +281 -0
- unrealon-0.1.20/src/unrealon/parsers/base.py +230 -0
- unrealon-0.1.20/src/unrealon/parsers/browser_parser.py +313 -0
- unrealon-0.1.20/src/unrealon/parsers/cli.py +388 -0
- unrealon-0.1.20/src/unrealon/parsers/monitor.py +147 -0
- unrealon-0.1.20/src/unrealon/parsers/storage.py +104 -0
- unrealon-0.1.20/src/unrealon/parsers/upload.py +311 -0
- unrealon-0.1.20/src/unrealon/parsers/utils/__init__.py +18 -0
- unrealon-0.1.20/src/unrealon/parsers/utils/cleaner.py +93 -0
- unrealon-0.1.20/src/unrealon/parsers/utils/notify.py +135 -0
- unrealon-0.1.20/src/unrealon/parsers/utils/ocr.py +186 -0
- unrealon-0.1.20/src/unrealon/runner.py +190 -0
- unrealon-0.1.18/github/README.md +0 -209
- {unrealon-0.1.18 → unrealon-0.1.20}/.gitignore +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/README.md +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/examples/README.md +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/github/unrealon/_api/generated/services/pyproject.toml +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/helpers/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/helpers/logger.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/helpers/retry.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/pyproject.toml +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/sync_client.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_config.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_constants.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/lifecycle.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/signals.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/state.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/exceptions/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/exceptions/handlers.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/exceptions/types.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_config.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_connection.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_constants.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_handlers.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_logging.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_messaging.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_metrics.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_reconnect.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_registration.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_types.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/circuit_breaker.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/unrealon_pb2.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/unrealon_pb2.pyi +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/unrealon_pb2_grpc.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/stream_service.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_config.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_formatters.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_handlers.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_logger.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_project.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/models/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/scheduling/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/scheduling/_manager.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/scheduling/_models.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/services/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/utils/__init__.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/utils/metrics.py +0 -0
- {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/utils/system.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unrealon
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.20
|
|
4
4
|
Summary: Unrealon SDK - Service management for Django backend (registration, heartbeat, logging, commands)
|
|
5
5
|
Project-URL: Homepage, https://github.com/markolofsen/unrealon-sdk
|
|
6
6
|
Project-URL: Documentation, https://unrealon.com
|
|
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: click>=8.1.0
|
|
21
|
+
Requires-Dist: cmdop
|
|
20
22
|
Requires-Dist: croniter<7.0.0,>=6.0.0
|
|
21
23
|
Requires-Dist: grpcio-tools<2.0.0,>=1.76.0
|
|
22
24
|
Requires-Dist: grpcio<2.0.0,>=1.76.0
|
|
@@ -26,6 +28,8 @@ Requires-Dist: psutil>=6.0.0
|
|
|
26
28
|
Requires-Dist: pydantic-settings>=2.7.0
|
|
27
29
|
Requires-Dist: pydantic<3.0.0,>=2.10.0
|
|
28
30
|
Requires-Dist: rich<15.0.0,>=14.3.1
|
|
31
|
+
Requires-Dist: sdkrouter
|
|
32
|
+
Requires-Dist: sdkrouter-tools
|
|
29
33
|
Requires-Dist: tenacity>=9.1.0
|
|
30
34
|
Provides-Extra: dev
|
|
31
35
|
Requires-Dist: build>=1.2.0; extra == 'dev'
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Unrealon SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for monitoring and managing services via the Unrealon platform.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Monitoring** — real-time service status visibility
|
|
8
|
+
- **Cloud Logs** — all logs accessible in the web interface
|
|
9
|
+
- **Control** — pause/resume/stop directly from the dashboard
|
|
10
|
+
- **Metrics** — counters for processed items and errors
|
|
11
|
+
- **Scheduling** — automatic cron-based task execution
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install unrealon
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Minimal Example
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from unrealon import ServiceClient
|
|
25
|
+
|
|
26
|
+
with ServiceClient(api_key="pk_xxx", service_name="my-service") as client:
|
|
27
|
+
client.info("Started")
|
|
28
|
+
|
|
29
|
+
for item in items:
|
|
30
|
+
process(item)
|
|
31
|
+
client.increment_processed()
|
|
32
|
+
|
|
33
|
+
client.info("Done")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The service will register, logs will stream to cloud, and metrics will be displayed.
|
|
37
|
+
|
|
38
|
+
### With Pause/Resume Support
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from unrealon import ServiceClient
|
|
42
|
+
|
|
43
|
+
with ServiceClient(api_key="pk_xxx", service_name="my-parser") as client:
|
|
44
|
+
client.info("Started")
|
|
45
|
+
|
|
46
|
+
for item in items:
|
|
47
|
+
client.check_interrupt() # Parser pauses here if Pause is clicked
|
|
48
|
+
|
|
49
|
+
process(item)
|
|
50
|
+
client.increment_processed()
|
|
51
|
+
|
|
52
|
+
client.info("Done")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`check_interrupt()` does two things:
|
|
56
|
+
- If **Pause** was clicked — waits until Resume
|
|
57
|
+
- If **Stop** was clicked — raises `StopInterrupt`
|
|
58
|
+
|
|
59
|
+
## Continuous Mode
|
|
60
|
+
|
|
61
|
+
A service that waits for commands from the dashboard:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import time
|
|
65
|
+
from unrealon import ServiceClient
|
|
66
|
+
from unrealon.exceptions import StopInterrupt
|
|
67
|
+
|
|
68
|
+
with ServiceClient(api_key="pk_xxx", service_name="my-parser") as client:
|
|
69
|
+
|
|
70
|
+
def handle_run(params: dict) -> dict:
|
|
71
|
+
limit = params.get("limit", 100)
|
|
72
|
+
|
|
73
|
+
client.set_busy()
|
|
74
|
+
try:
|
|
75
|
+
for i in range(limit):
|
|
76
|
+
client.check_interrupt()
|
|
77
|
+
do_work()
|
|
78
|
+
client.increment_processed()
|
|
79
|
+
return {"status": "ok"}
|
|
80
|
+
except StopInterrupt:
|
|
81
|
+
return {"status": "stopped"}
|
|
82
|
+
finally:
|
|
83
|
+
client.set_idle()
|
|
84
|
+
|
|
85
|
+
client.on_command("run", handle_run)
|
|
86
|
+
|
|
87
|
+
# Wait for commands
|
|
88
|
+
client.set_idle()
|
|
89
|
+
while not client.should_stop:
|
|
90
|
+
time.sleep(1)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Now from the dashboard you can:
|
|
94
|
+
- Click **Run** — executes `handle_run`
|
|
95
|
+
- Click **Pause** — parser stops at `check_interrupt()`
|
|
96
|
+
- Click **Resume** — continues from where it left off
|
|
97
|
+
- Click **Stop** — graceful shutdown
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### Logging
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
client.debug("Debug message")
|
|
105
|
+
client.info("Info message", key="value")
|
|
106
|
+
client.warning("Warning")
|
|
107
|
+
client.error("Error", code=500)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Logs go to three places: console (Rich), file, and cloud.
|
|
111
|
+
|
|
112
|
+
### Metrics
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
client.increment_processed() # +1 processed
|
|
116
|
+
client.increment_processed(10) # +10 processed
|
|
117
|
+
client.increment_errors() # +1 error
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Status
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
client.set_busy() # Shows "Busy" in dashboard
|
|
124
|
+
client.set_idle() # Shows "Idle"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### State
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
client.is_paused # True if paused
|
|
131
|
+
client.should_stop # True if stop requested
|
|
132
|
+
client.is_connected # True if connected to server
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Commands
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# Register handler
|
|
139
|
+
client.on_command("run", handle_run)
|
|
140
|
+
client.on_command("custom", handle_custom)
|
|
141
|
+
|
|
142
|
+
# Handler receives params and returns result
|
|
143
|
+
def handle_run(params: dict) -> dict:
|
|
144
|
+
limit = params.get("limit", 10)
|
|
145
|
+
# ... do work ...
|
|
146
|
+
return {"status": "ok", "processed": 100}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Schedules
|
|
150
|
+
|
|
151
|
+
Schedules run automatically based on cron expressions.
|
|
152
|
+
If the schedule's `action_type` matches a registered command,
|
|
153
|
+
the same handler is used:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# This handler works for both manual Run and scheduled runs
|
|
157
|
+
client.on_command("run", handle_run)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For different behavior, register a schedule-specific handler:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
@client.on_schedule("process")
|
|
164
|
+
def handle_scheduled_process(schedule, params):
|
|
165
|
+
# schedule.name, schedule.id are available
|
|
166
|
+
return {"items_processed": 100}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Configuration
|
|
170
|
+
|
|
171
|
+
### Via Environment Variables
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
export UNREALON_API_KEY=pk_xxx
|
|
175
|
+
export UNREALON_SERVICE_NAME=my-service
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# Picks up from env
|
|
180
|
+
with ServiceClient() as client:
|
|
181
|
+
...
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Dev Mode (Local Server)
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
with ServiceClient(
|
|
188
|
+
api_key="dk_xxx",
|
|
189
|
+
service_name="my-service",
|
|
190
|
+
dev_mode=True, # Connects to localhost:50051
|
|
191
|
+
) as client:
|
|
192
|
+
...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Exceptions
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from unrealon.exceptions import (
|
|
199
|
+
StopInterrupt, # Stop requested (inherits BaseException!)
|
|
200
|
+
UnrealonError, # Base SDK error
|
|
201
|
+
AuthenticationError, # Bad API key
|
|
202
|
+
RegistrationError, # Can't register
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
with ServiceClient(...) as client:
|
|
207
|
+
for item in items:
|
|
208
|
+
client.check_interrupt()
|
|
209
|
+
process(item)
|
|
210
|
+
except StopInterrupt:
|
|
211
|
+
print("Stopped by command")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Important**: `StopInterrupt` inherits from `BaseException`, not `Exception`.
|
|
215
|
+
This means `except Exception` won't catch it — by design, so generic
|
|
216
|
+
error handlers don't swallow the stop command.
|
|
217
|
+
|
|
218
|
+
## Standalone Logger
|
|
219
|
+
|
|
220
|
+
You can use the logger separately from the SDK:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from unrealon.logging import get_logger
|
|
224
|
+
|
|
225
|
+
log = get_logger("myapp")
|
|
226
|
+
log.info("Starting", version="1.0")
|
|
227
|
+
log.error("Failed", error="connection timeout")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Logs go to console and file (without cloud).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unrealon"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.20"
|
|
8
8
|
description = "Unrealon SDK - Service management for Django backend (registration, heartbeat, logging, commands)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -35,6 +35,10 @@ dependencies = [
|
|
|
35
35
|
"protobuf (>=6.33.5,<7.0.0)",
|
|
36
36
|
"rich (>=14.3.1,<15.0.0)",
|
|
37
37
|
"croniter (>=6.0.0,<7.0.0)",
|
|
38
|
+
"click>=8.1.0",
|
|
39
|
+
"cmdop",
|
|
40
|
+
"sdkrouter",
|
|
41
|
+
"sdkrouter-tools",
|
|
38
42
|
]
|
|
39
43
|
|
|
40
44
|
[project.optional-dependencies]
|
|
@@ -93,3 +97,9 @@ strict = true
|
|
|
93
97
|
asyncio_mode = "auto"
|
|
94
98
|
testpaths = ["tests"]
|
|
95
99
|
pythonpath = ["src"]
|
|
100
|
+
|
|
101
|
+
# Local development dependencies (for parsers module)
|
|
102
|
+
[tool.uv.sources]
|
|
103
|
+
cmdop = { path = "../../../../../@projects/cmdop/projects/software/cmdop_sdk/libs/sdk_python", editable = true }
|
|
104
|
+
sdkrouter = { path = "../../../../../@projects/sdkrouter/solution/packages/sdkrouter_py", editable = true }
|
|
105
|
+
sdkrouter-tools = { path = "../../../../../@projects/sdkrouter/solution/packages/sdkrouter_tools_py", editable = true }
|
|
@@ -53,6 +53,7 @@ from .exceptions import (
|
|
|
53
53
|
from .grpc import GRPCStreamService
|
|
54
54
|
from .logging import get_logger
|
|
55
55
|
from .models import ServiceStatus
|
|
56
|
+
from .runner import TaskRunner
|
|
56
57
|
from .scheduling import Schedule, ScheduleResult, ScheduleRunStatus
|
|
57
58
|
|
|
58
59
|
__all__ = [
|
|
@@ -96,4 +97,8 @@ __all__ = [
|
|
|
96
97
|
"Schedule",
|
|
97
98
|
"ScheduleResult",
|
|
98
99
|
"ScheduleRunStatus",
|
|
100
|
+
# Runner
|
|
101
|
+
"TaskRunner",
|
|
102
|
+
# Parsers submodule (import as: from unrealon.parsers import ...)
|
|
103
|
+
# Note: parsers module requires optional dependencies: pip install unrealon[parsers]
|
|
99
104
|
]
|
|
@@ -36,6 +36,7 @@ from .enums import (
|
|
|
36
36
|
CommandCommandType,
|
|
37
37
|
CommandStatus,
|
|
38
38
|
LogEntryRequestLevel,
|
|
39
|
+
PatchedScheduleCreateRequestActionType,
|
|
39
40
|
PatchedServiceDetailRequestConnectionType,
|
|
40
41
|
ProcessJobJobType,
|
|
41
42
|
ProcessJobStatus,
|
|
@@ -285,6 +286,7 @@ __all__ = [
|
|
|
285
286
|
"CommandCommandType",
|
|
286
287
|
"CommandStatus",
|
|
287
288
|
"LogEntryRequestLevel",
|
|
289
|
+
"PatchedScheduleCreateRequestActionType",
|
|
288
290
|
"PatchedServiceDetailRequestConnectionType",
|
|
289
291
|
"ProcessJobJobType",
|
|
290
292
|
"ProcessJobStatus",
|
|
@@ -99,6 +99,22 @@ class LogEntryRequestLevel(StrEnum):
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
class PatchedScheduleCreateRequestActionType(StrEnum):
|
|
103
|
+
"""
|
|
104
|
+
Action type to execute
|
|
105
|
+
* `run` - Run (execute parser)
|
|
106
|
+
* `pause` - Pause service
|
|
107
|
+
* `resume` - Resume service
|
|
108
|
+
* `custom` - Custom action
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
RUN = "run"
|
|
112
|
+
PAUSE = "pause"
|
|
113
|
+
RESUME = "resume"
|
|
114
|
+
CUSTOM = "custom"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
102
118
|
class PatchedServiceDetailRequestConnectionType(StrEnum):
|
|
103
119
|
"""
|
|
104
120
|
How to control this service (local, cloud, ssh, or none)
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
|
-
from ..enums import ScheduleEventEventType, ScheduleRunStatus
|
|
9
|
+
from ..enums import PatchedScheduleCreateRequestActionType, ScheduleEventEventType, ScheduleRunStatus
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Schedule(BaseModel):
|
|
@@ -34,10 +34,9 @@ class Schedule(BaseModel):
|
|
|
34
34
|
max_length=100,
|
|
35
35
|
)
|
|
36
36
|
frequency_display: Any = ...
|
|
37
|
-
action_type:
|
|
37
|
+
action_type: PatchedScheduleCreateRequestActionType | None = Field(
|
|
38
38
|
None,
|
|
39
|
-
description='Action type to execute
|
|
40
|
-
max_length=50,
|
|
39
|
+
description='Action type to execute * `run` ...',
|
|
41
40
|
)
|
|
42
41
|
next_run_at: str | None = Field(None, description='Next scheduled execution time')
|
|
43
42
|
last_run_at: Any | None = Field(None, description='Last execution time')
|
|
@@ -105,11 +104,9 @@ class ScheduleCreateRequest(BaseModel):
|
|
|
105
104
|
min_length=1,
|
|
106
105
|
max_length=50,
|
|
107
106
|
)
|
|
108
|
-
action_type:
|
|
107
|
+
action_type: PatchedScheduleCreateRequestActionType | None = Field(
|
|
109
108
|
None,
|
|
110
|
-
description='Action type to execute
|
|
111
|
-
min_length=1,
|
|
112
|
-
max_length=50,
|
|
109
|
+
description='Action type to execute * `run` ...',
|
|
113
110
|
)
|
|
114
111
|
action_params: dict[str, Any] | None = Field(
|
|
115
112
|
None,
|
|
@@ -163,10 +160,9 @@ class ScheduleCreate(BaseModel):
|
|
|
163
160
|
description="Timezone for schedule (e.g., 'Eu...",
|
|
164
161
|
max_length=50,
|
|
165
162
|
)
|
|
166
|
-
action_type:
|
|
163
|
+
action_type: PatchedScheduleCreateRequestActionType | None = Field(
|
|
167
164
|
None,
|
|
168
|
-
description='Action type to execute
|
|
169
|
-
max_length=50,
|
|
165
|
+
description='Action type to execute * `run` ...',
|
|
170
166
|
)
|
|
171
167
|
action_params: dict[str, Any] | None = Field(
|
|
172
168
|
None,
|
|
@@ -223,10 +219,9 @@ class ScheduleDetail(BaseModel):
|
|
|
223
219
|
description="Timezone for schedule (e.g., 'Eu...",
|
|
224
220
|
max_length=50,
|
|
225
221
|
)
|
|
226
|
-
action_type:
|
|
222
|
+
action_type: PatchedScheduleCreateRequestActionType | None = Field(
|
|
227
223
|
None,
|
|
228
|
-
description='Action type to execute
|
|
229
|
-
max_length=50,
|
|
224
|
+
description='Action type to execute * `run` ...',
|
|
230
225
|
)
|
|
231
226
|
action_params: dict[str, Any] | None = Field(
|
|
232
227
|
None,
|
|
@@ -294,11 +289,9 @@ class PatchedScheduleCreateRequest(BaseModel):
|
|
|
294
289
|
min_length=1,
|
|
295
290
|
max_length=50,
|
|
296
291
|
)
|
|
297
|
-
action_type:
|
|
292
|
+
action_type: PatchedScheduleCreateRequestActionType | None = Field(
|
|
298
293
|
None,
|
|
299
|
-
description='Action type to execute
|
|
300
|
-
min_length=1,
|
|
301
|
-
max_length=50,
|
|
294
|
+
description='Action type to execute * `run` ...',
|
|
302
295
|
)
|
|
303
296
|
action_params: dict[str, Any] | None = Field(
|
|
304
297
|
None,
|
|
@@ -79,6 +79,7 @@ class ServiceClient:
|
|
|
79
79
|
"_logger",
|
|
80
80
|
"_cloud_handler",
|
|
81
81
|
"_resume_event",
|
|
82
|
+
"_log_level",
|
|
82
83
|
)
|
|
83
84
|
|
|
84
85
|
def __init__(
|
|
@@ -94,6 +95,7 @@ class ServiceClient:
|
|
|
94
95
|
heartbeat_interval: int | None = None,
|
|
95
96
|
log_batch_size: int | None = None,
|
|
96
97
|
log_flush_interval: float | None = None,
|
|
98
|
+
log_level: str = "INFO",
|
|
97
99
|
) -> None:
|
|
98
100
|
"""
|
|
99
101
|
Initialize service client.
|
|
@@ -109,6 +111,7 @@ class ServiceClient:
|
|
|
109
111
|
heartbeat_interval: Heartbeat interval in seconds
|
|
110
112
|
log_batch_size: Number of logs to batch before sending
|
|
111
113
|
log_flush_interval: Max seconds to wait before flushing logs
|
|
114
|
+
log_level: Minimum log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
112
115
|
"""
|
|
113
116
|
config_kwargs: dict[str, object] = {}
|
|
114
117
|
if api_key:
|
|
@@ -151,11 +154,14 @@ class ServiceClient:
|
|
|
151
154
|
self._resume_event.set() # Start as "not paused" (event is set)
|
|
152
155
|
|
|
153
156
|
# Initialize logger with Rich console + file, cloud handler added on start
|
|
157
|
+
self._log_level = log_level.upper()
|
|
154
158
|
self._logger: UnrealonLogger = get_logger(
|
|
155
159
|
name=self._config.service_name,
|
|
160
|
+
level=self._log_level, # type: ignore[arg-type]
|
|
156
161
|
log_to_cloud=False, # Will be connected after gRPC start
|
|
157
162
|
)
|
|
158
163
|
self._cloud_handler: CloudHandler = CloudHandler()
|
|
164
|
+
self._cloud_handler.setLevel(getattr(logging, self._log_level))
|
|
159
165
|
|
|
160
166
|
@property
|
|
161
167
|
def grpc(self) -> GRPCStreamService:
|
|
@@ -603,10 +609,17 @@ class ServiceClient:
|
|
|
603
609
|
|
|
604
610
|
def _setup_signal_handlers(self) -> None:
|
|
605
611
|
"""Setup graceful shutdown signal handlers."""
|
|
612
|
+
import os
|
|
606
613
|
|
|
607
614
|
def signal_handler(signum: int, _frame: FrameType | None) -> None:
|
|
615
|
+
if self._shutdown_requested:
|
|
616
|
+
# Second signal - force exit immediately
|
|
617
|
+
logger.info("Received signal %d again, forcing exit...", signum)
|
|
618
|
+
os._exit(1)
|
|
608
619
|
logger.info("Received signal %d, requesting shutdown...", signum)
|
|
609
620
|
self._shutdown_requested = True
|
|
621
|
+
# Unblock any waiting threads
|
|
622
|
+
self._resume_event.set()
|
|
610
623
|
|
|
611
624
|
try:
|
|
612
625
|
self._original_sigint = signal.signal(signal.SIGINT, signal_handler)
|
|
@@ -631,6 +644,7 @@ class AsyncServiceClient:
|
|
|
631
644
|
"_grpc",
|
|
632
645
|
"_logger",
|
|
633
646
|
"_cloud_handler",
|
|
647
|
+
"_log_level",
|
|
634
648
|
)
|
|
635
649
|
|
|
636
650
|
def __init__(
|
|
@@ -643,6 +657,7 @@ class AsyncServiceClient:
|
|
|
643
657
|
dev_mode: bool = False,
|
|
644
658
|
source_code: str | None = None,
|
|
645
659
|
description: str | None = None,
|
|
660
|
+
log_level: str = "INFO",
|
|
646
661
|
) -> None:
|
|
647
662
|
"""Initialize async service client."""
|
|
648
663
|
config_kwargs: dict[str, object] = {}
|
|
@@ -674,11 +689,14 @@ class AsyncServiceClient:
|
|
|
674
689
|
self._grpc: GRPCStreamService | None = None
|
|
675
690
|
|
|
676
691
|
# Initialize logger with Rich console + file, cloud handler added on start
|
|
692
|
+
self._log_level = log_level.upper()
|
|
677
693
|
self._logger: UnrealonLogger = get_logger(
|
|
678
694
|
name=self._config.service_name,
|
|
695
|
+
level=self._log_level, # type: ignore[arg-type]
|
|
679
696
|
log_to_cloud=False,
|
|
680
697
|
)
|
|
681
698
|
self._cloud_handler: CloudHandler = CloudHandler()
|
|
699
|
+
self._cloud_handler.setLevel(getattr(logging, self._log_level))
|
|
682
700
|
|
|
683
701
|
@property
|
|
684
702
|
def grpc(self) -> GRPCStreamService:
|