uptic 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.
uptic-0.1.0/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ dist
2
+ __pycache__
3
+ *.egg-info
uptic-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vedus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
uptic-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.5
2
+ Name: uptic
3
+ Version: 0.1.0
4
+ Summary: Report errors and structured logs to Uptic from Python scripts and services
5
+ Project-URL: Homepage, https://uptic.run
6
+ Project-URL: Repository, https://github.com/thevedus/uptic
7
+ Project-URL: Issues, https://github.com/thevedus/uptic/issues
8
+ Author-email: Vedus <hello@vedus.in>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: error-tracking,logging,observability,uptic
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: System :: Monitoring
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+
19
+ # uptic
20
+
21
+ Report errors and structured logs to Uptic from Python.
22
+
23
+ No runtime dependencies — one `urllib` POST.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install uptic
29
+ ```
30
+
31
+ ## Inside an Uptic script
32
+
33
+ The script runner injects credentials, so there is nothing to configure:
34
+
35
+ ```python
36
+ from uptic import uptic
37
+
38
+ def fetch(payload, context):
39
+ client = uptic()
40
+
41
+ client.info("starting", {"trigger": context.get("type")})
42
+
43
+ with client.catching():
44
+ result = do_the_work(payload)
45
+
46
+ client.info("done", {"count": len(result)})
47
+ return result
48
+ ```
49
+
50
+ `catching()` reports whatever is raised and then re-raises, so the execution is
51
+ still recorded as failed. Pass `reraise=False` to swallow it instead.
52
+
53
+ ## In a service
54
+
55
+ There is no injected environment, so pass a **public ingest key** (`pk_…`,
56
+ created under Settings → API Keys → Public Ingest Keys). Never use an `sk_` API
57
+ key here — it carries full workspace permissions.
58
+
59
+ ```python
60
+ from uptic import UpticClient
61
+
62
+ client = UpticClient(
63
+ base_url="https://app.uptic.run",
64
+ ingest_key="pk_your_public_key",
65
+ service="billing-worker",
66
+ environment="production",
67
+ release=os.environ.get("GIT_SHA"),
68
+ )
69
+
70
+ try:
71
+ charge(order)
72
+ except Exception as err:
73
+ client.capture_error(err, tags={"order": order.id})
74
+ raise
75
+ ```
76
+
77
+ ## API
78
+
79
+ | Method | What it does |
80
+ | --- | --- |
81
+ | `uptic(**config)` | Get (and configure) the shared client |
82
+ | `capture_error(error, **options)` | Report one error. Returns `True` if accepted |
83
+ | `capture_batch(events)` | Report several in one request |
84
+ | `catching(reraise=True, **options)` | Context manager: report, then re-raise |
85
+ | `log(level, message, meta=None)` | One JSON line to stdout/stderr |
86
+ | `debug` / `info` / `warn` / `error` | Shorthands for `log` |
87
+ | `config(name, fallback=None)` | Read an injected env value |
88
+
89
+ `capture_error` accepts an exception or a plain string, plus `level`, `tags`,
90
+ `context`, `fingerprint` and `service`.
91
+
92
+ ### Config
93
+
94
+ Every field falls back to an environment variable, which is what the script
95
+ runner sets:
96
+
97
+ | Argument | Env var |
98
+ | --- | --- |
99
+ | `base_url` | `UPTIC_BASE_URL` |
100
+ | `ingest_key` | `UPTIC_INGEST_KEY` |
101
+ | `service` | `UPTIC_WORKER_ID` (else `"default"`) |
102
+ | `environment` | `UPTIC_ENVIRONMENT` |
103
+ | `release` | `UPTIC_RELEASE` |
104
+
105
+ ## Two guarantees
106
+
107
+ **Reporting never raises.** `capture_error` returns `False` on a network
108
+ failure. A monitoring call that takes down the thing it monitors is worse than
109
+ a missing data point.
110
+
111
+ **Logging makes no network call.** `log()` writes one JSON line to
112
+ stdout/stderr; the script runner already captures both into the execution
113
+ record, so logging works offline and costs nothing.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ python -m pytest packages/python-sdk/tests -q
119
+ ```
uptic-0.1.0/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # uptic
2
+
3
+ Report errors and structured logs to Uptic from Python.
4
+
5
+ No runtime dependencies — one `urllib` POST.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install uptic
11
+ ```
12
+
13
+ ## Inside an Uptic script
14
+
15
+ The script runner injects credentials, so there is nothing to configure:
16
+
17
+ ```python
18
+ from uptic import uptic
19
+
20
+ def fetch(payload, context):
21
+ client = uptic()
22
+
23
+ client.info("starting", {"trigger": context.get("type")})
24
+
25
+ with client.catching():
26
+ result = do_the_work(payload)
27
+
28
+ client.info("done", {"count": len(result)})
29
+ return result
30
+ ```
31
+
32
+ `catching()` reports whatever is raised and then re-raises, so the execution is
33
+ still recorded as failed. Pass `reraise=False` to swallow it instead.
34
+
35
+ ## In a service
36
+
37
+ There is no injected environment, so pass a **public ingest key** (`pk_…`,
38
+ created under Settings → API Keys → Public Ingest Keys). Never use an `sk_` API
39
+ key here — it carries full workspace permissions.
40
+
41
+ ```python
42
+ from uptic import UpticClient
43
+
44
+ client = UpticClient(
45
+ base_url="https://app.uptic.run",
46
+ ingest_key="pk_your_public_key",
47
+ service="billing-worker",
48
+ environment="production",
49
+ release=os.environ.get("GIT_SHA"),
50
+ )
51
+
52
+ try:
53
+ charge(order)
54
+ except Exception as err:
55
+ client.capture_error(err, tags={"order": order.id})
56
+ raise
57
+ ```
58
+
59
+ ## API
60
+
61
+ | Method | What it does |
62
+ | --- | --- |
63
+ | `uptic(**config)` | Get (and configure) the shared client |
64
+ | `capture_error(error, **options)` | Report one error. Returns `True` if accepted |
65
+ | `capture_batch(events)` | Report several in one request |
66
+ | `catching(reraise=True, **options)` | Context manager: report, then re-raise |
67
+ | `log(level, message, meta=None)` | One JSON line to stdout/stderr |
68
+ | `debug` / `info` / `warn` / `error` | Shorthands for `log` |
69
+ | `config(name, fallback=None)` | Read an injected env value |
70
+
71
+ `capture_error` accepts an exception or a plain string, plus `level`, `tags`,
72
+ `context`, `fingerprint` and `service`.
73
+
74
+ ### Config
75
+
76
+ Every field falls back to an environment variable, which is what the script
77
+ runner sets:
78
+
79
+ | Argument | Env var |
80
+ | --- | --- |
81
+ | `base_url` | `UPTIC_BASE_URL` |
82
+ | `ingest_key` | `UPTIC_INGEST_KEY` |
83
+ | `service` | `UPTIC_WORKER_ID` (else `"default"`) |
84
+ | `environment` | `UPTIC_ENVIRONMENT` |
85
+ | `release` | `UPTIC_RELEASE` |
86
+
87
+ ## Two guarantees
88
+
89
+ **Reporting never raises.** `capture_error` returns `False` on a network
90
+ failure. A monitoring call that takes down the thing it monitors is worse than
91
+ a missing data point.
92
+
93
+ **Logging makes no network call.** `log()` writes one JSON line to
94
+ stdout/stderr; the script runner already captures both into the execution
95
+ record, so logging works offline and costs nothing.
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ python -m pytest packages/python-sdk/tests -q
101
+ ```
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "uptic"
7
+ version = "0.1.0"
8
+ description = "Report errors and structured logs to Uptic from Python scripts and services"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Vedus", email = "hello@vedus.in" }]
13
+ keywords = ["uptic", "error-tracking", "logging", "observability"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: System :: Monitoring",
19
+ ]
20
+ # No runtime dependencies on purpose: this gets installed into whatever
21
+ # environment a user's script already has, and urllib covers one POST.
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Homepage = "https://uptic.run"
26
+ Repository = "https://github.com/thevedus/uptic"
27
+ Issues = "https://github.com/thevedus/uptic/issues"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/uptic"]
31
+
32
+ [tool.hatch.build.targets.sdist]
33
+ include = ["src/uptic", "README.md", "LICENSE"]
@@ -0,0 +1,211 @@
1
+ """uptic — report errors and structured logs to Uptic from Python.
2
+
3
+ Mirrors ``uptic-sdk`` (Node/Bun) so the two do not drift: same env
4
+ contract, same wire shape, same no-throw guarantee.
5
+
6
+ Inside the Uptic script runner this needs no configuration — the runner
7
+ injects ``UPTIC_BASE_URL`` and ``UPTIC_INGEST_KEY``.
8
+
9
+ from uptic import uptic
10
+
11
+ def fetch(payload, context):
12
+ client = uptic()
13
+ client.info("starting", {"payload": payload})
14
+ with client.catching():
15
+ do_the_work()
16
+ return {"ok": True}
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import sys
24
+ import time
25
+ import traceback
26
+ import urllib.error
27
+ import urllib.request
28
+ from contextlib import contextmanager
29
+ from datetime import datetime, timezone
30
+ from typing import Any, Dict, Iterator, List, Optional
31
+
32
+ __all__ = ["UpticClient", "uptic", "capture_error", "log"]
33
+ __version__ = "0.1.0"
34
+
35
+ _INGEST_PATH = "/open/error/report/public"
36
+ _DEFAULT_TIMEOUT = 5.0
37
+
38
+
39
+ def _env(name: str) -> Optional[str]:
40
+ value = os.environ.get(name)
41
+ return value or None
42
+
43
+
44
+ class UpticClient:
45
+ """A configured reporter. Construct once and reuse."""
46
+
47
+ def __init__(
48
+ self,
49
+ base_url: Optional[str] = None,
50
+ ingest_key: Optional[str] = None,
51
+ service: Optional[str] = None,
52
+ environment: Optional[str] = None,
53
+ release: Optional[str] = None,
54
+ tags: Optional[Dict[str, str]] = None,
55
+ timeout: float = _DEFAULT_TIMEOUT,
56
+ ) -> None:
57
+ self.base_url = (base_url or _env("UPTIC_BASE_URL") or "").rstrip("/")
58
+ self.ingest_key = ingest_key or _env("UPTIC_INGEST_KEY") or ""
59
+ self.service = service or _env("UPTIC_WORKER_ID") or "default"
60
+ self.environment = environment or _env("UPTIC_ENVIRONMENT")
61
+ self.release = release or _env("UPTIC_RELEASE")
62
+ self.tags = tags or {}
63
+ self.timeout = timeout
64
+
65
+ @property
66
+ def is_configured(self) -> bool:
67
+ """Whether reporting can actually reach a server."""
68
+ return bool(self.base_url and self.ingest_key)
69
+
70
+ def build_event(
71
+ self,
72
+ error: BaseException | str,
73
+ level: str = "error",
74
+ tags: Optional[Dict[str, str]] = None,
75
+ context: Optional[Dict[str, Any]] = None,
76
+ fingerprint: Optional[List[str]] = None,
77
+ service: Optional[str] = None,
78
+ ) -> Dict[str, Any]:
79
+ if isinstance(error, BaseException):
80
+ message = str(error) or error.__class__.__name__
81
+ event_type = error.__class__.__name__
82
+ stack = "".join(
83
+ traceback.format_exception(
84
+ type(error), error, error.__traceback__
85
+ )
86
+ )
87
+ else:
88
+ message = str(error)
89
+ event_type = None
90
+ stack = None
91
+
92
+ event: Dict[str, Any] = {
93
+ "service": service or self.service,
94
+ "level": level,
95
+ "message": message,
96
+ "timestamp": int(time.time() * 1000),
97
+ }
98
+ if event_type:
99
+ event["type"] = event_type
100
+ if stack:
101
+ event["stack"] = stack
102
+ if self.environment:
103
+ event["environment"] = self.environment
104
+ if self.release:
105
+ event["release"] = self.release
106
+ if fingerprint:
107
+ event["fingerprint"] = fingerprint
108
+ if context:
109
+ event["context"] = context
110
+
111
+ merged_tags = {**self.tags, **(tags or {})}
112
+ if merged_tags:
113
+ event["tags"] = merged_tags
114
+
115
+ return event
116
+
117
+ def capture_error(self, error: BaseException | str, **kwargs: Any) -> bool:
118
+ """Report one error. Never raises — a monitoring call that takes down
119
+ the thing it monitors is worse than a missing data point."""
120
+ return self.capture_batch([self.build_event(error, **kwargs)])
121
+
122
+ def capture_batch(self, events: List[Dict[str, Any]]) -> bool:
123
+ if not self.is_configured or not events:
124
+ return False
125
+
126
+ body = json.dumps({"key": self.ingest_key, "events": events}).encode(
127
+ "utf-8"
128
+ )
129
+ request = urllib.request.Request(
130
+ self.base_url + _INGEST_PATH,
131
+ data=body,
132
+ headers={"Content-Type": "application/json"},
133
+ method="POST",
134
+ )
135
+
136
+ try:
137
+ with urllib.request.urlopen(request, timeout=self.timeout) as resp:
138
+ return 200 <= resp.status < 300
139
+ except Exception:
140
+ # includes HTTPError, URLError, socket timeouts and anything else
141
+ return False
142
+
143
+ def log(
144
+ self,
145
+ level: str,
146
+ message: str,
147
+ meta: Optional[Dict[str, Any]] = None,
148
+ ) -> None:
149
+ """Structured logging: one JSON line on stdout/stderr. The script
150
+ runner already captures both into the execution record, so this makes
151
+ no network call and works offline."""
152
+ line = {
153
+ "ts": datetime.now(timezone.utc).isoformat(),
154
+ "level": level,
155
+ "service": self.service,
156
+ "message": message,
157
+ }
158
+ if meta is not None:
159
+ line["meta"] = meta
160
+
161
+ stream = sys.stderr if level in ("warn", "error") else sys.stdout
162
+ print(json.dumps(line, default=str), file=stream, flush=True)
163
+
164
+ def debug(self, message: str, meta: Optional[Dict[str, Any]] = None) -> None:
165
+ self.log("debug", message, meta)
166
+
167
+ def info(self, message: str, meta: Optional[Dict[str, Any]] = None) -> None:
168
+ self.log("info", message, meta)
169
+
170
+ def warn(self, message: str, meta: Optional[Dict[str, Any]] = None) -> None:
171
+ self.log("warn", message, meta)
172
+
173
+ def error(self, message: str, meta: Optional[Dict[str, Any]] = None) -> None:
174
+ self.log("error", message, meta)
175
+
176
+ def config(self, name: str, fallback: Optional[str] = None) -> Optional[str]:
177
+ """Read an injected config value. Scripts get a deliberately narrow
178
+ environment, so this is a readable accessor rather than a pretence of
179
+ a secret store."""
180
+ return _env(name) or fallback
181
+
182
+ @contextmanager
183
+ def catching(self, reraise: bool = True, **kwargs: Any) -> Iterator[None]:
184
+ """Report anything raised inside the block, then re-raise by default
185
+ so the script still fails loudly."""
186
+ try:
187
+ yield
188
+ except BaseException as err: # noqa: BLE001 - reported, then re-raised
189
+ self.capture_error(err, **kwargs)
190
+ if reraise:
191
+ raise
192
+
193
+
194
+ _default_client: Optional[UpticClient] = None
195
+
196
+
197
+ def uptic(**kwargs: Any) -> UpticClient:
198
+ """The zero-config entry point. Inside the Uptic script runner this needs
199
+ no arguments — credentials are already in the environment."""
200
+ global _default_client
201
+ if kwargs or _default_client is None:
202
+ _default_client = UpticClient(**kwargs)
203
+ return _default_client
204
+
205
+
206
+ def capture_error(error: BaseException | str, **kwargs: Any) -> bool:
207
+ return uptic().capture_error(error, **kwargs)
208
+
209
+
210
+ def log(level: str, message: str, meta: Optional[Dict[str, Any]] = None) -> None:
211
+ uptic().log(level, message, meta)