aethellayer 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.
- aethellayer-0.1.0/.gitignore +46 -0
- aethellayer-0.1.0/PKG-INFO +85 -0
- aethellayer-0.1.0/README.md +57 -0
- aethellayer-0.1.0/aethellayer/__init__.py +157 -0
- aethellayer-0.1.0/aethellayer/cli.py +62 -0
- aethellayer-0.1.0/pyproject.toml +40 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
|
|
16
|
+
# next.js
|
|
17
|
+
/.next/
|
|
18
|
+
/out/
|
|
19
|
+
|
|
20
|
+
# production
|
|
21
|
+
/build
|
|
22
|
+
|
|
23
|
+
# misc
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.pem
|
|
26
|
+
.venv/
|
|
27
|
+
packages/python/dist/
|
|
28
|
+
packages/python/*.egg-info/
|
|
29
|
+
packages/sdk/dist/
|
|
30
|
+
|
|
31
|
+
# debug
|
|
32
|
+
npm-debug.log*
|
|
33
|
+
yarn-debug.log*
|
|
34
|
+
yarn-error.log*
|
|
35
|
+
.pnpm-debug.log*
|
|
36
|
+
|
|
37
|
+
# env files (can opt-in for committing if needed)
|
|
38
|
+
.env*
|
|
39
|
+
!.env.example
|
|
40
|
+
|
|
41
|
+
# vercel
|
|
42
|
+
.vercel
|
|
43
|
+
|
|
44
|
+
# typescript
|
|
45
|
+
*.tsbuildinfo
|
|
46
|
+
next-env.d.ts
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aethellayer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AethelLayer probe for FastAPI, Starlette, and Python CLIs. Catch silent onboarding failures and open an inbox thread.
|
|
5
|
+
Project-URL: Homepage, https://aethellayer.com
|
|
6
|
+
Project-URL: Documentation, https://aethellayer.com
|
|
7
|
+
Project-URL: Repository, https://github.com/aethellayer/aeth
|
|
8
|
+
Author: AethelLayer
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: aethellayer,developer-experience,fastapi,observability,sdk
|
|
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.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build; extra == 'dev'
|
|
23
|
+
Requires-Dist: twine; extra == 'dev'
|
|
24
|
+
Provides-Extra: fastapi
|
|
25
|
+
Requires-Dist: fastapi>=0.110; extra == 'fastapi'
|
|
26
|
+
Requires-Dist: starlette>=0.36; extra == 'fastapi'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# aethellayer (Python)
|
|
30
|
+
|
|
31
|
+
Probe for AethelLayer. Catch silent FastAPI, Starlette, Flask-style, and CLI failures.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install aethellayer
|
|
37
|
+
export AETHEL_API_KEY=aethel_live_...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## FastAPI
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from fastapi import FastAPI
|
|
44
|
+
from aethellayer import Probe
|
|
45
|
+
|
|
46
|
+
app = FastAPI()
|
|
47
|
+
probe = Probe.from_env(environment="api_middleware", app_name="payments-api")
|
|
48
|
+
app.add_middleware(probe.fastapi_middleware())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Capture manually
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
probe.capture(
|
|
55
|
+
error_code="RATE_LIMIT_429",
|
|
56
|
+
endpoint_path="GET /v1/keys",
|
|
57
|
+
taxonomy="RateLimit",
|
|
58
|
+
message="Starter quota burned before noon",
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## CLI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
aethellayer init
|
|
66
|
+
aethellayer run -- python your_cli.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or in code:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from aethellayer import Probe
|
|
73
|
+
|
|
74
|
+
with Probe.from_env(environment="cli").wrap_cli():
|
|
75
|
+
main()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Publish (maintainers)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cd packages/python
|
|
82
|
+
python -m pip install build twine
|
|
83
|
+
python -m build
|
|
84
|
+
python -m twine upload dist/*
|
|
85
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# aethellayer (Python)
|
|
2
|
+
|
|
3
|
+
Probe for AethelLayer. Catch silent FastAPI, Starlette, Flask-style, and CLI failures.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install aethellayer
|
|
9
|
+
export AETHEL_API_KEY=aethel_live_...
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## FastAPI
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from fastapi import FastAPI
|
|
16
|
+
from aethellayer import Probe
|
|
17
|
+
|
|
18
|
+
app = FastAPI()
|
|
19
|
+
probe = Probe.from_env(environment="api_middleware", app_name="payments-api")
|
|
20
|
+
app.add_middleware(probe.fastapi_middleware())
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Capture manually
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
probe.capture(
|
|
27
|
+
error_code="RATE_LIMIT_429",
|
|
28
|
+
endpoint_path="GET /v1/keys",
|
|
29
|
+
taxonomy="RateLimit",
|
|
30
|
+
message="Starter quota burned before noon",
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CLI
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
aethellayer init
|
|
38
|
+
aethellayer run -- python your_cli.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or in code:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from aethellayer import Probe
|
|
45
|
+
|
|
46
|
+
with Probe.from_env(environment="cli").wrap_cli():
|
|
47
|
+
main()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Publish (maintainers)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd packages/python
|
|
54
|
+
python -m pip install build twine
|
|
55
|
+
python -m build
|
|
56
|
+
python -m twine upload dist/*
|
|
57
|
+
```
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""AethelLayer Python probe."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Callable, Mapping, MutableMapping, Optional
|
|
13
|
+
|
|
14
|
+
DEFAULT_ENDPOINT = "https://api.aethellayer.com/v1/probes"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Probe:
|
|
19
|
+
api_key: str
|
|
20
|
+
environment: str = "api_middleware"
|
|
21
|
+
endpoint: str = DEFAULT_ENDPOINT
|
|
22
|
+
app_name: Optional[str] = None
|
|
23
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
if not self.api_key:
|
|
27
|
+
raise ValueError("Probe requires api_key (AETHEL_API_KEY)")
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_env(cls, **kwargs: Any) -> "Probe":
|
|
31
|
+
key = kwargs.pop("api_key", None) or os.environ.get("AETHEL_API_KEY")
|
|
32
|
+
if not key:
|
|
33
|
+
raise ValueError("Set AETHEL_API_KEY or pass api_key=")
|
|
34
|
+
return cls(api_key=key, **kwargs)
|
|
35
|
+
|
|
36
|
+
def capture(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
error_code: str,
|
|
40
|
+
message: Optional[str] = None,
|
|
41
|
+
stack: Optional[str] = None,
|
|
42
|
+
endpoint_path: Optional[str] = None,
|
|
43
|
+
taxonomy: Optional[str] = None,
|
|
44
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
body = {
|
|
47
|
+
"error_code": error_code,
|
|
48
|
+
"stack_trace": stack or message,
|
|
49
|
+
"endpoint_path": endpoint_path,
|
|
50
|
+
"taxonomy_category": taxonomy,
|
|
51
|
+
"environment": self.environment,
|
|
52
|
+
"app_name": self.app_name,
|
|
53
|
+
"metadata": {
|
|
54
|
+
"runtime": f"python {sys.version.split()[0]}",
|
|
55
|
+
**dict(self.metadata),
|
|
56
|
+
**dict(metadata or {}),
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
data = json.dumps(body).encode("utf-8")
|
|
60
|
+
req = urllib.request.Request(
|
|
61
|
+
self.endpoint,
|
|
62
|
+
data=data,
|
|
63
|
+
method="POST",
|
|
64
|
+
headers={
|
|
65
|
+
"content-type": "application/json",
|
|
66
|
+
"authorization": f"Bearer {self.api_key}",
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
with urllib.request.urlopen(req, timeout=5) as res:
|
|
71
|
+
return {"ok": True, "status": getattr(res, "status", 200)}
|
|
72
|
+
except urllib.error.HTTPError as err:
|
|
73
|
+
return {"ok": False, "status": err.code}
|
|
74
|
+
except Exception as err: # noqa: BLE001 - probe must never crash the host
|
|
75
|
+
return {"ok": False, "status": 0, "error": str(err)}
|
|
76
|
+
|
|
77
|
+
def capture_exception(
|
|
78
|
+
self,
|
|
79
|
+
exc: BaseException,
|
|
80
|
+
*,
|
|
81
|
+
error_code: str = "UNCAUGHT",
|
|
82
|
+
endpoint_path: Optional[str] = None,
|
|
83
|
+
taxonomy: str = "Uncaught",
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
return self.capture(
|
|
86
|
+
error_code=error_code,
|
|
87
|
+
message=str(exc),
|
|
88
|
+
stack="".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
|
|
89
|
+
endpoint_path=endpoint_path,
|
|
90
|
+
taxonomy=taxonomy,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def fastapi_middleware(self):
|
|
94
|
+
"""Starlette/FastAPI BaseHTTPMiddleware that captures 5xx responses."""
|
|
95
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
96
|
+
from starlette.requests import Request
|
|
97
|
+
from starlette.responses import Response
|
|
98
|
+
|
|
99
|
+
probe = self
|
|
100
|
+
|
|
101
|
+
class AethelMiddleware(BaseHTTPMiddleware):
|
|
102
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
103
|
+
try:
|
|
104
|
+
response = await call_next(request)
|
|
105
|
+
except Exception as exc: # noqa: BLE001
|
|
106
|
+
probe.capture_exception(
|
|
107
|
+
exc,
|
|
108
|
+
error_code="UNCAUGHT_MIDDLEWARE",
|
|
109
|
+
endpoint_path=f"{request.method} {request.url.path}",
|
|
110
|
+
)
|
|
111
|
+
raise
|
|
112
|
+
if response.status_code >= 500:
|
|
113
|
+
probe.capture(
|
|
114
|
+
error_code=f"HTTP_{response.status_code}",
|
|
115
|
+
endpoint_path=f"{request.method} {request.url.path}",
|
|
116
|
+
taxonomy="Http5xx",
|
|
117
|
+
)
|
|
118
|
+
return response
|
|
119
|
+
|
|
120
|
+
return AethelMiddleware
|
|
121
|
+
|
|
122
|
+
def wrap_cli(self, argv: Optional[list[str]] = None):
|
|
123
|
+
"""Context manager / decorator helper for CLI entrypoints."""
|
|
124
|
+
argv = argv or sys.argv
|
|
125
|
+
probe = self
|
|
126
|
+
|
|
127
|
+
class _Session:
|
|
128
|
+
def __enter__(self):
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def __exit__(self, exc_type, exc, tb):
|
|
132
|
+
if exc is not None:
|
|
133
|
+
probe.capture_exception(
|
|
134
|
+
exc,
|
|
135
|
+
error_code="CLI_FAILURE",
|
|
136
|
+
endpoint_path=" ".join(argv),
|
|
137
|
+
taxonomy="Cli",
|
|
138
|
+
)
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def run(self, fn: Callable[[], Any]) -> Any:
|
|
142
|
+
try:
|
|
143
|
+
return fn()
|
|
144
|
+
except Exception as exc: # noqa: BLE001
|
|
145
|
+
probe.capture_exception(
|
|
146
|
+
exc,
|
|
147
|
+
error_code="CLI_FAILURE",
|
|
148
|
+
endpoint_path=" ".join(argv),
|
|
149
|
+
taxonomy="Cli",
|
|
150
|
+
)
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
return _Session()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def probe(**kwargs: Any) -> Probe:
|
|
157
|
+
return Probe.from_env(**kwargs) if "api_key" not in kwargs else Probe(**kwargs)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CLI for the aethellayer Python package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import Probe
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> None:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="aethellayer", description="AethelLayer Python probe")
|
|
15
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
16
|
+
|
|
17
|
+
sub.add_parser("init", help="Print install steps")
|
|
18
|
+
run = sub.add_parser("run", help="Wrap a command and capture non-zero exits")
|
|
19
|
+
run.add_argument("command", nargs=argparse.REMAINDER, help="Command after --")
|
|
20
|
+
|
|
21
|
+
args = parser.parse_args(argv)
|
|
22
|
+
if args.cmd == "init" or args.cmd is None:
|
|
23
|
+
print(
|
|
24
|
+
"""AethelLayer Python init
|
|
25
|
+
|
|
26
|
+
1. pip install aethellayer
|
|
27
|
+
2. export AETHEL_API_KEY=aethel_live_...
|
|
28
|
+
3. FastAPI:
|
|
29
|
+
from aethellayer import Probe
|
|
30
|
+
app.add_middleware(Probe.from_env(app_name="api").fastapi_middleware())
|
|
31
|
+
4. CLI:
|
|
32
|
+
aethellayer run -- python your_cli.py
|
|
33
|
+
|
|
34
|
+
Open the dashboard inbox after the first failure.
|
|
35
|
+
"""
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if args.cmd == "run":
|
|
40
|
+
command = [c for c in args.command if c != "--"]
|
|
41
|
+
if not command:
|
|
42
|
+
print("Usage: aethellayer run -- <command> [...args]", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
api_key = os.environ.get("AETHEL_API_KEY")
|
|
45
|
+
if not api_key:
|
|
46
|
+
print("Missing AETHEL_API_KEY", file=sys.stderr)
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
probe = Probe(api_key=api_key, environment="cli")
|
|
49
|
+
result = subprocess.run(command)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
probe.capture(
|
|
52
|
+
error_code=f"CLI_EXIT_{result.returncode}",
|
|
53
|
+
endpoint_path=" ".join(command),
|
|
54
|
+
taxonomy="Cli",
|
|
55
|
+
)
|
|
56
|
+
sys.exit(result.returncode)
|
|
57
|
+
|
|
58
|
+
parser.print_help()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aethellayer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AethelLayer probe for FastAPI, Starlette, and Python CLIs. Catch silent onboarding failures and open an inbox thread."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "AethelLayer" }]
|
|
13
|
+
keywords = ["aethellayer", "sdk", "observability", "fastapi", "developer-experience"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
fastapi = ["fastapi>=0.110", "starlette>=0.36"]
|
|
29
|
+
dev = ["build", "twine"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://aethellayer.com"
|
|
33
|
+
Documentation = "https://aethellayer.com"
|
|
34
|
+
Repository = "https://github.com/aethellayer/aeth"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
aethellayer = "aethellayer.cli:main"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["aethellayer"]
|