peekapi 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,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ insert_final_newline = true
6
+ charset = utf-8
7
+ indent_style = space
8
+ indent_size = 4
9
+ trim_trailing_whitespace = true
peekapi-0.1.0/.git ADDED
@@ -0,0 +1 @@
1
+ gitdir: ../../.git/modules/packages/sdk-python
@@ -0,0 +1 @@
1
+ github: peekapi-dev
@@ -0,0 +1,39 @@
1
+ name: Bug Report
2
+ description: Report a bug
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: description
7
+ attributes:
8
+ label: Description
9
+ description: A clear description of the bug.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: steps
14
+ attributes:
15
+ label: Steps to Reproduce
16
+ description: Steps to reproduce the behavior.
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: expected
21
+ attributes:
22
+ label: Expected Behavior
23
+ description: What you expected to happen.
24
+ validations:
25
+ required: true
26
+ - type: input
27
+ id: sdk-version
28
+ attributes:
29
+ label: SDK Version
30
+ placeholder: "0.1.0"
31
+ validations:
32
+ required: true
33
+ - type: input
34
+ id: runtime-version
35
+ attributes:
36
+ label: Python Version
37
+ placeholder: "3.13"
38
+ validations:
39
+ required: true
@@ -0,0 +1,25 @@
1
+ name: Feature Request
2
+ description: Suggest a feature
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: description
7
+ attributes:
8
+ label: Description
9
+ description: What would you like to see added?
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: use-case
14
+ attributes:
15
+ label: Use Case
16
+ description: Why do you need this feature?
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: proposed-solution
21
+ attributes:
22
+ label: Proposed Solution
23
+ description: How would you like it to work?
24
+ validations:
25
+ required: false
@@ -0,0 +1,8 @@
1
+ ## Summary
2
+
3
+ <!-- What does this PR do? -->
4
+
5
+ ## Checklist
6
+
7
+ - [ ] Tests pass
8
+ - [ ] No breaking changes (or documented in description)
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.13"
17
+ - run: pip install uv
18
+ - run: uv sync --no-install-project
19
+ - run: uv run pytest -v
20
+ - run: uv run ruff check src/ tests/
21
+ - run: uv run ruff format --check src/ tests/
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ dist/
5
+ *.egg-info/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [0.1.0] - 2025-06-01
8
+
9
+ - Initial release
@@ -0,0 +1,5 @@
1
+ # Code of Conduct
2
+
3
+ This project follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
4
+
5
+ Please report unacceptable behavior to **security@peekapi.dev**.
peekapi-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PeekAPI
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.
peekapi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: peekapi
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency Python SDK for PeekAPI
5
+ Project-URL: Homepage, https://github.com/peekapi-dev/sdk-python
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: analytics,api,dashboard,middleware
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Framework :: Flask
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # PeekAPI — Python SDK
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/peekapi)](https://pypi.org/project/peekapi/)
27
+ [![license](https://img.shields.io/pypi/l/peekapi)](./LICENSE)
28
+ [![CI](https://github.com/peekapi-dev/sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/peekapi-dev/sdk-python/actions/workflows/ci.yml)
29
+
30
+ Zero-dependency Python SDK for [PeekAPI](https://peekapi.dev). Built-in middleware for ASGI (FastAPI, Starlette, Litestar), WSGI (Flask, Bottle), and Django.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install peekapi
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ### FastAPI / Starlette (ASGI)
41
+
42
+ ```python
43
+ from fastapi import FastAPI
44
+ from peekapi import PeekApiClient
45
+ from peekapi.middleware import PeekApiASGI
46
+
47
+ client = PeekApiClient({"api_key": "ak_live_xxx"})
48
+
49
+ app = FastAPI()
50
+ app.add_middleware(PeekApiASGI, client=client)
51
+ ```
52
+
53
+ ### Flask (WSGI)
54
+
55
+ ```python
56
+ from flask import Flask
57
+ from peekapi import PeekApiClient
58
+ from peekapi.middleware import PeekApiWSGI
59
+
60
+ client = PeekApiClient({"api_key": "ak_live_xxx"})
61
+
62
+ app = Flask(__name__)
63
+ app.wsgi_app = PeekApiWSGI(app.wsgi_app, client=client)
64
+ ```
65
+
66
+ ### Django
67
+
68
+ ```python
69
+ # settings.py
70
+ PEEKAPI = {
71
+ "api_key": "ak_live_xxx",
72
+ }
73
+
74
+ MIDDLEWARE = [
75
+ "peekapi.middleware.django.PeekApiMiddleware",
76
+ # ... other middleware
77
+ ]
78
+ ```
79
+
80
+ ### Standalone Client
81
+
82
+ ```python
83
+ from peekapi import PeekApiClient
84
+
85
+ client = PeekApiClient({"api_key": "ak_live_xxx"})
86
+
87
+ client.track({
88
+ "method": "GET",
89
+ "path": "/api/users",
90
+ "status_code": 200,
91
+ "response_time_ms": 42,
92
+ })
93
+
94
+ # Graceful shutdown (flushes remaining events)
95
+ client.shutdown()
96
+ ```
97
+
98
+ ## Configuration
99
+
100
+ | Option | Default | Description |
101
+ |---|---|---|
102
+ | `api_key` | required | Your PeekAPI key |
103
+ | `endpoint` | PeekAPI cloud | Ingestion endpoint URL |
104
+ | `flush_interval` | `10.0` | Seconds between automatic flushes |
105
+ | `batch_size` | `100` | Events per HTTP POST (triggers flush) |
106
+ | `max_buffer_size` | `10000` | Max events held in memory |
107
+ | `max_storage_bytes` | `5242880` | Max disk fallback file size (5MB) |
108
+ | `max_event_bytes` | `65536` | Per-event size limit (64KB) |
109
+ | `storage_path` | auto | Custom path for JSONL persistence file |
110
+ | `debug` | `False` | Enable debug logging |
111
+ | `on_error` | `None` | Callback `(Exception) -> None` for flush errors |
112
+
113
+ ## How It Works
114
+
115
+ 1. Middleware intercepts every request/response
116
+ 2. Captures method, path, status code, response time, request/response sizes, consumer ID
117
+ 3. Events are buffered in memory and flushed in batches on a daemon thread
118
+ 4. On network failure: exponential backoff with jitter, up to 5 retries
119
+ 5. After max retries: events are persisted to a JSONL file on disk
120
+ 6. On next startup: persisted events are recovered and re-sent
121
+ 7. On SIGTERM/SIGINT: remaining buffer is flushed or persisted to disk
122
+
123
+ ## Consumer Identification
124
+
125
+ By default, consumers are identified by:
126
+
127
+ 1. `X-API-Key` header — stored as-is
128
+ 2. `Authorization` header — hashed with SHA-256 (stored as `hash_<hex>`)
129
+
130
+ Override with the `identify_consumer` option to use any header or request property:
131
+
132
+ ```python
133
+ client = PeekApiClient({
134
+ "api_key": "...",
135
+ "identify_consumer": lambda headers: headers.get("x-tenant-id"),
136
+ })
137
+ ```
138
+
139
+ The callback receives a `dict[str, str]` of lowercase header names and should return a consumer ID string or `None`.
140
+
141
+ ## Features
142
+
143
+ - **Zero runtime dependencies** — uses only Python stdlib
144
+ - **Background flush** — daemon thread with configurable interval and batch size
145
+ - **Disk persistence** — undelivered events saved to JSONL, recovered on restart
146
+ - **Exponential backoff** — with jitter, max 5 consecutive failures before disk fallback
147
+ - **SSRF protection** — private IP blocking, HTTPS enforcement (HTTP only for localhost)
148
+ - **Input sanitization** — path (2048), method (16), consumer_id (256) truncation
149
+ - **Per-event size limit** — strips metadata first, drops if still too large (default 64KB)
150
+ - **Graceful shutdown** — signal handlers (SIGTERM/SIGINT) with disk persistence
151
+
152
+ ## Requirements
153
+
154
+ - Python >= 3.10
155
+
156
+ ## Contributing
157
+
158
+ 1. Fork & clone the repo
159
+ 2. Install dev dependencies — `uv sync --no-install-project`
160
+ 3. Run tests — `uv run pytest -v`
161
+ 4. Lint & format — `uv run ruff check src/ tests/` / `uv run ruff format src/ tests/`
162
+ 5. Submit a PR
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,143 @@
1
+ # PeekAPI — Python SDK
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/peekapi)](https://pypi.org/project/peekapi/)
4
+ [![license](https://img.shields.io/pypi/l/peekapi)](./LICENSE)
5
+ [![CI](https://github.com/peekapi-dev/sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/peekapi-dev/sdk-python/actions/workflows/ci.yml)
6
+
7
+ Zero-dependency Python SDK for [PeekAPI](https://peekapi.dev). Built-in middleware for ASGI (FastAPI, Starlette, Litestar), WSGI (Flask, Bottle), and Django.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install peekapi
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### FastAPI / Starlette (ASGI)
18
+
19
+ ```python
20
+ from fastapi import FastAPI
21
+ from peekapi import PeekApiClient
22
+ from peekapi.middleware import PeekApiASGI
23
+
24
+ client = PeekApiClient({"api_key": "ak_live_xxx"})
25
+
26
+ app = FastAPI()
27
+ app.add_middleware(PeekApiASGI, client=client)
28
+ ```
29
+
30
+ ### Flask (WSGI)
31
+
32
+ ```python
33
+ from flask import Flask
34
+ from peekapi import PeekApiClient
35
+ from peekapi.middleware import PeekApiWSGI
36
+
37
+ client = PeekApiClient({"api_key": "ak_live_xxx"})
38
+
39
+ app = Flask(__name__)
40
+ app.wsgi_app = PeekApiWSGI(app.wsgi_app, client=client)
41
+ ```
42
+
43
+ ### Django
44
+
45
+ ```python
46
+ # settings.py
47
+ PEEKAPI = {
48
+ "api_key": "ak_live_xxx",
49
+ }
50
+
51
+ MIDDLEWARE = [
52
+ "peekapi.middleware.django.PeekApiMiddleware",
53
+ # ... other middleware
54
+ ]
55
+ ```
56
+
57
+ ### Standalone Client
58
+
59
+ ```python
60
+ from peekapi import PeekApiClient
61
+
62
+ client = PeekApiClient({"api_key": "ak_live_xxx"})
63
+
64
+ client.track({
65
+ "method": "GET",
66
+ "path": "/api/users",
67
+ "status_code": 200,
68
+ "response_time_ms": 42,
69
+ })
70
+
71
+ # Graceful shutdown (flushes remaining events)
72
+ client.shutdown()
73
+ ```
74
+
75
+ ## Configuration
76
+
77
+ | Option | Default | Description |
78
+ |---|---|---|
79
+ | `api_key` | required | Your PeekAPI key |
80
+ | `endpoint` | PeekAPI cloud | Ingestion endpoint URL |
81
+ | `flush_interval` | `10.0` | Seconds between automatic flushes |
82
+ | `batch_size` | `100` | Events per HTTP POST (triggers flush) |
83
+ | `max_buffer_size` | `10000` | Max events held in memory |
84
+ | `max_storage_bytes` | `5242880` | Max disk fallback file size (5MB) |
85
+ | `max_event_bytes` | `65536` | Per-event size limit (64KB) |
86
+ | `storage_path` | auto | Custom path for JSONL persistence file |
87
+ | `debug` | `False` | Enable debug logging |
88
+ | `on_error` | `None` | Callback `(Exception) -> None` for flush errors |
89
+
90
+ ## How It Works
91
+
92
+ 1. Middleware intercepts every request/response
93
+ 2. Captures method, path, status code, response time, request/response sizes, consumer ID
94
+ 3. Events are buffered in memory and flushed in batches on a daemon thread
95
+ 4. On network failure: exponential backoff with jitter, up to 5 retries
96
+ 5. After max retries: events are persisted to a JSONL file on disk
97
+ 6. On next startup: persisted events are recovered and re-sent
98
+ 7. On SIGTERM/SIGINT: remaining buffer is flushed or persisted to disk
99
+
100
+ ## Consumer Identification
101
+
102
+ By default, consumers are identified by:
103
+
104
+ 1. `X-API-Key` header — stored as-is
105
+ 2. `Authorization` header — hashed with SHA-256 (stored as `hash_<hex>`)
106
+
107
+ Override with the `identify_consumer` option to use any header or request property:
108
+
109
+ ```python
110
+ client = PeekApiClient({
111
+ "api_key": "...",
112
+ "identify_consumer": lambda headers: headers.get("x-tenant-id"),
113
+ })
114
+ ```
115
+
116
+ The callback receives a `dict[str, str]` of lowercase header names and should return a consumer ID string or `None`.
117
+
118
+ ## Features
119
+
120
+ - **Zero runtime dependencies** — uses only Python stdlib
121
+ - **Background flush** — daemon thread with configurable interval and batch size
122
+ - **Disk persistence** — undelivered events saved to JSONL, recovered on restart
123
+ - **Exponential backoff** — with jitter, max 5 consecutive failures before disk fallback
124
+ - **SSRF protection** — private IP blocking, HTTPS enforcement (HTTP only for localhost)
125
+ - **Input sanitization** — path (2048), method (16), consumer_id (256) truncation
126
+ - **Per-event size limit** — strips metadata first, drops if still too large (default 64KB)
127
+ - **Graceful shutdown** — signal handlers (SIGTERM/SIGINT) with disk persistence
128
+
129
+ ## Requirements
130
+
131
+ - Python >= 3.10
132
+
133
+ ## Contributing
134
+
135
+ 1. Fork & clone the repo
136
+ 2. Install dev dependencies — `uv sync --no-install-project`
137
+ 3. Run tests — `uv run pytest -v`
138
+ 4. Lint & format — `uv run ruff check src/ tests/` / `uv run ruff format src/ tests/`
139
+ 5. Submit a PR
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,7 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please report security vulnerabilities to **security@peekapi.dev**.
6
+
7
+ Do not open a public issue. We will acknowledge your report within 48 hours and aim to release a fix within 7 days for critical issues.
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "peekapi"
7
+ dynamic = ["version"]
8
+ description = "Zero-dependency Python SDK for PeekAPI"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ keywords = ["api", "analytics", "middleware", "dashboard"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Framework :: Django",
24
+ "Framework :: FastAPI",
25
+ "Framework :: Flask",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/peekapi-dev/sdk-python"
30
+
31
+ [tool.hatch.version]
32
+ path = "src/peekapi/_version.py"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/peekapi"]
36
+
37
+ [dependency-groups]
38
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.25", "ruff>=0.11"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+ pythonpath = ["src"]
43
+
44
+ [tool.ruff]
45
+ target-version = "py310"
46
+ line-length = 100
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
@@ -0,0 +1,17 @@
1
+ """PeekAPI — Python SDK."""
2
+
3
+ from ._consumer import default_identify_consumer, hash_consumer_id
4
+ from .client import PeekApiClient
5
+ from .middleware import PeekApiASGI, PeekApiMiddleware, PeekApiWSGI
6
+ from .types import Options, RequestEvent
7
+
8
+ __all__ = [
9
+ "Options",
10
+ "PeekApiASGI",
11
+ "PeekApiClient",
12
+ "PeekApiMiddleware",
13
+ "PeekApiWSGI",
14
+ "RequestEvent",
15
+ "default_identify_consumer",
16
+ "hash_consumer_id",
17
+ ]
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+
5
+
6
+ def hash_consumer_id(raw: str) -> str:
7
+ """SHA-256 hash truncated to 12 hex chars, prefixed with 'hash_'."""
8
+ digest = hashlib.sha256(raw.encode()).hexdigest()[:12]
9
+ return f"hash_{digest}"
10
+
11
+
12
+ def default_identify_consumer(headers: dict[str, str]) -> str | None:
13
+ """Identify consumer from request headers.
14
+
15
+ Priority:
16
+ 1. x-api-key (stored as-is)
17
+ 2. Authorization (hashed — contains credentials)
18
+ """
19
+ api_key = headers.get("x-api-key")
20
+ if api_key:
21
+ return api_key
22
+
23
+ auth = headers.get("authorization")
24
+ if auth:
25
+ return hash_consumer_id(auth)
26
+
27
+ return None
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ import re
5
+ from urllib.parse import urlparse
6
+
7
+ # Matches private/reserved IPv4 ranges (fast path)
8
+ _PRIVATE_IP_RE = re.compile(
9
+ r"^(?:"
10
+ r"10\.\d{1,3}\.\d{1,3}\.\d{1,3}"
11
+ r"|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}"
12
+ r"|192\.168\.\d{1,3}\.\d{1,3}"
13
+ r"|0\.0\.0\.0"
14
+ r")$"
15
+ )
16
+
17
+ # CGNAT range not covered by ipaddress.is_private in all Python versions
18
+ _CGNAT_NETWORK = ipaddress.IPv4Network("100.64.0.0/10")
19
+
20
+
21
+ def is_private_ip(host: str) -> bool:
22
+ """Check if a hostname/IP is a private or reserved address.
23
+
24
+ Covers: RFC 1918, CGNAT (100.64/10), loopback, link-local,
25
+ IPv6 ULA/link-local, IPv4-mapped IPv6.
26
+ """
27
+ # Fast path regex
28
+ if _PRIVATE_IP_RE.match(host):
29
+ return True
30
+
31
+ try:
32
+ addr = ipaddress.ip_address(host)
33
+ except ValueError:
34
+ return False
35
+
36
+ if isinstance(addr, ipaddress.IPv6Address):
37
+ # Check IPv4-mapped IPv6 (::ffff:x.x.x.x)
38
+ mapped = addr.ipv4_mapped
39
+ if mapped is not None:
40
+ return mapped.is_private or mapped.is_loopback or mapped.is_link_local
41
+ return addr.is_private or addr.is_loopback or addr.is_link_local
42
+
43
+ # IPv4 — is_private covers RFC 1918 + loopback + link-local; add CGNAT explicitly
44
+ return addr.is_private or addr.is_loopback or addr.is_link_local or addr in _CGNAT_NETWORK
45
+
46
+
47
+ def validate_endpoint(endpoint: str) -> str:
48
+ """Validate and normalize the ingestion endpoint URL.
49
+
50
+ Raises ValueError for:
51
+ - Non-HTTPS URLs (except localhost)
52
+ - Private/reserved IP addresses (SSRF protection)
53
+ - Embedded credentials in URL
54
+ - Malformed URLs
55
+ """
56
+ if not endpoint:
57
+ raise ValueError("endpoint is required")
58
+
59
+ parsed = urlparse(endpoint)
60
+
61
+ if not parsed.scheme or not parsed.hostname:
62
+ raise ValueError(f"Invalid endpoint URL: {endpoint}")
63
+
64
+ hostname = parsed.hostname.lower()
65
+
66
+ # Allow HTTP only for localhost
67
+ is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
68
+
69
+ if parsed.scheme != "https" and not is_localhost:
70
+ raise ValueError(f"HTTPS required for non-localhost endpoint: {endpoint}")
71
+
72
+ # Reject embedded credentials
73
+ if parsed.username or parsed.password:
74
+ raise ValueError("Endpoint URL must not contain credentials")
75
+
76
+ # SSRF check — skip for localhost
77
+ if not is_localhost and is_private_ip(hostname):
78
+ raise ValueError(f"Endpoint resolves to private/reserved IP: {hostname}")
79
+
80
+ return endpoint
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"