vertodigital 0.2.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VertoDigital
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.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: vertodigital
3
+ Version: 0.2.0
4
+ Summary: Python client for VertoDigital's public agent API.
5
+ Author-email: VertoDigital <team@vertodigital.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://vertodigital.com
8
+ Project-URL: Documentation, https://vertodigital.com/developers
9
+ Project-URL: Changelog, https://pypi.org/project/vertodigital/#history
10
+ Keywords: vertodigital,api,sdk,agent,mcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.txt
24
+ Dynamic: license-file
25
+
26
+ # vertodigital
27
+
28
+ Official Python client for [VertoDigital's public agent API](https://vertodigital.com/developers). Every endpoint is anonymous, so there's no API key to set up. No runtime dependencies, just the Python standard library.
29
+
30
+ ## Install
31
+
32
+ ```
33
+ pip install vertodigital
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from vertodigital import Client
40
+
41
+ client = Client()
42
+
43
+ client.health()
44
+ # {'status': 'ok', 'service': 'vertodigital-agent-api', ...}
45
+
46
+ # One page of case studies
47
+ page = client.case_studies(limit=10)
48
+ for cs in page["items"]:
49
+ print(f'{cs["name"]}: {cs["stat"]}')
50
+
51
+ # Or every case study, paginated for you
52
+ urls = [cs["url"] for cs in client.iter_case_studies()]
53
+
54
+ # Any page on vertodigital.com as markdown
55
+ print(client.page("about"))
56
+ print(client.llms_txt())
57
+
58
+ # The OpenAPI 3.1 spec
59
+ list(client.openapi()["paths"])
60
+
61
+ # Submit a contact enquiry on behalf of a user
62
+ client.contact(
63
+ from_name="Ada Lovelace",
64
+ from_email="ada@example.com",
65
+ organization="Analytical Engines Ltd",
66
+ topic="services", # assessment, services, partnership, press, careers, general
67
+ message="We'd like to discuss a marketing analytics engagement.",
68
+ )
69
+ ```
70
+
71
+ `contact` sends an `Idempotency-Key` automatically. If you retry after a timeout, pass the same `idempotency_key=` again so the enquiry is only recorded once.
72
+
73
+ Requests go to the pinned `/agent/v1/*` paths by default. Pass `api_version=None` to use the unversioned `/agent/*` paths.
74
+
75
+ ## Errors
76
+
77
+ - `vertodigital.APIError` for any non-2xx response. It carries `status` and `body`.
78
+ - `vertodigital.RateLimitError` (a subclass) on 429. It adds `retry_after` in seconds.
79
+ - `vertodigital.ValidationError` when input fails validation before a request is sent.
80
+
81
+ All three inherit from `vertodigital.VertoDigitalError`. Rate limits and the full API reference are at [vertodigital.com/developers](https://vertodigital.com/developers).
82
+
83
+ ## Changelog
84
+
85
+ - **0.2.0** - Requires Python 3.11 or later (3.8, 3.9 and 3.10 have reached end of life). No API changes.
86
+ - **0.1.0** - First release: `health`, `case_studies` / `iter_case_studies`, `contact`, `openapi`, `llms_txt`, `page`.
87
+
88
+ ## Publishing (maintainers)
89
+
90
+ From this directory:
91
+
92
+ ```
93
+ python3 -m pip install --upgrade build twine
94
+ python3 -m build
95
+ python3 -m twine upload dist/*
96
+ ```
97
+
98
+ Bump `__version__` in `src/vertodigital/_version.py` and add a changelog entry on every release. Upload with a PyPI API token (username `__token__`).
@@ -0,0 +1,73 @@
1
+ # vertodigital
2
+
3
+ Official Python client for [VertoDigital's public agent API](https://vertodigital.com/developers). Every endpoint is anonymous, so there's no API key to set up. No runtime dependencies, just the Python standard library.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ pip install vertodigital
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from vertodigital import Client
15
+
16
+ client = Client()
17
+
18
+ client.health()
19
+ # {'status': 'ok', 'service': 'vertodigital-agent-api', ...}
20
+
21
+ # One page of case studies
22
+ page = client.case_studies(limit=10)
23
+ for cs in page["items"]:
24
+ print(f'{cs["name"]}: {cs["stat"]}')
25
+
26
+ # Or every case study, paginated for you
27
+ urls = [cs["url"] for cs in client.iter_case_studies()]
28
+
29
+ # Any page on vertodigital.com as markdown
30
+ print(client.page("about"))
31
+ print(client.llms_txt())
32
+
33
+ # The OpenAPI 3.1 spec
34
+ list(client.openapi()["paths"])
35
+
36
+ # Submit a contact enquiry on behalf of a user
37
+ client.contact(
38
+ from_name="Ada Lovelace",
39
+ from_email="ada@example.com",
40
+ organization="Analytical Engines Ltd",
41
+ topic="services", # assessment, services, partnership, press, careers, general
42
+ message="We'd like to discuss a marketing analytics engagement.",
43
+ )
44
+ ```
45
+
46
+ `contact` sends an `Idempotency-Key` automatically. If you retry after a timeout, pass the same `idempotency_key=` again so the enquiry is only recorded once.
47
+
48
+ Requests go to the pinned `/agent/v1/*` paths by default. Pass `api_version=None` to use the unversioned `/agent/*` paths.
49
+
50
+ ## Errors
51
+
52
+ - `vertodigital.APIError` for any non-2xx response. It carries `status` and `body`.
53
+ - `vertodigital.RateLimitError` (a subclass) on 429. It adds `retry_after` in seconds.
54
+ - `vertodigital.ValidationError` when input fails validation before a request is sent.
55
+
56
+ All three inherit from `vertodigital.VertoDigitalError`. Rate limits and the full API reference are at [vertodigital.com/developers](https://vertodigital.com/developers).
57
+
58
+ ## Changelog
59
+
60
+ - **0.2.0** - Requires Python 3.11 or later (3.8, 3.9 and 3.10 have reached end of life). No API changes.
61
+ - **0.1.0** - First release: `health`, `case_studies` / `iter_case_studies`, `contact`, `openapi`, `llms_txt`, `page`.
62
+
63
+ ## Publishing (maintainers)
64
+
65
+ From this directory:
66
+
67
+ ```
68
+ python3 -m pip install --upgrade build twine
69
+ python3 -m build
70
+ python3 -m twine upload dist/*
71
+ ```
72
+
73
+ Bump `__version__` in `src/vertodigital/_version.py` and add a changelog entry on every release. Upload with a PyPI API token (username `__token__`).
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vertodigital"
7
+ dynamic = ["version"]
8
+ description = "Python client for VertoDigital's public agent API."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE.txt"]
12
+ authors = [{ name = "VertoDigital", email = "team@vertodigital.com" }]
13
+ requires-python = ">=3.11"
14
+ keywords = ["vertodigital", "api", "sdk", "agent", "mcp"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Operating System :: OS Independent",
25
+ "Typing :: Typed",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://vertodigital.com"
30
+ Documentation = "https://vertodigital.com/developers"
31
+ Changelog = "https://pypi.org/project/vertodigital/#history"
32
+
33
+ [tool.setuptools.dynamic]
34
+ version = { attr = "vertodigital._version.__version__" }
35
+
36
+ [tool.setuptools.package-data]
37
+ vertodigital = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """Python client for VertoDigital's public agent API. See https://vertodigital.com/developers."""
2
+
3
+ from ._version import __version__
4
+ from .client import Client
5
+ from .errors import APIError, RateLimitError, ValidationError, VertoDigitalError
6
+
7
+ __all__ = ["Client", "APIError", "RateLimitError", "ValidationError", "VertoDigitalError", "__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,158 @@
1
+ """Client for VertoDigital's public agent API (https://vertodigital.com/developers).
2
+
3
+ Every endpoint is anonymous - no API key needed.
4
+ """
5
+
6
+ import json
7
+ import re
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ import uuid
12
+ from typing import Any, Dict, Iterator, Optional
13
+
14
+ from ._version import __version__
15
+ from .errors import APIError, RateLimitError, ValidationError
16
+
17
+ DEFAULT_BASE_URL = "https://vertodigital.com"
18
+ TOPICS = ("assessment", "services", "partnership", "press", "careers", "general")
19
+
20
+
21
+ class Client:
22
+ """VertoDigital agent API client.
23
+
24
+ base_url - override for testing against a staging deploy.
25
+ timeout - request timeout in seconds.
26
+ api_version - "v1" pins requests to /agent/v1/*; None uses the unversioned /agent/* paths.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ base_url: str = DEFAULT_BASE_URL,
32
+ timeout: float = 15,
33
+ api_version: Optional[str] = "v1",
34
+ ) -> None:
35
+ self.base_url = base_url.rstrip("/")
36
+ self.timeout = timeout
37
+ self._prefix = f"/agent/{api_version}" if api_version else "/agent"
38
+
39
+ def health(self) -> Dict[str, Any]:
40
+ """GET /agent/v1/health - service status and endpoint map."""
41
+ return self._request("GET", f"{self._prefix}/health")
42
+
43
+ def case_studies(self, limit: Optional[int] = None, cursor: Optional[str] = None) -> Dict[str, Any]:
44
+ """GET /agent/v1/case-studies - one page of case studies.
45
+
46
+ Returns the raw page: {"items": [...], "count", "total", "has_more", "next_cursor"}.
47
+ """
48
+ query = {}
49
+ if limit is not None:
50
+ query["limit"] = limit
51
+ if cursor is not None:
52
+ query["cursor"] = cursor
53
+ return self._request("GET", f"{self._prefix}/case-studies", query=query)
54
+
55
+ def iter_case_studies(self, limit: int = 50) -> Iterator[Dict[str, Any]]:
56
+ """Yield every case study across all pages."""
57
+ cursor = None
58
+ while True:
59
+ page = self.case_studies(limit=limit, cursor=cursor)
60
+ yield from page.get("items", [])
61
+ if not (page.get("has_more") and page.get("next_cursor")):
62
+ return
63
+ cursor = page["next_cursor"]
64
+
65
+ def contact(
66
+ self,
67
+ from_name: str,
68
+ from_email: str,
69
+ message: str,
70
+ organization: Optional[str] = None,
71
+ topic: str = "general",
72
+ agent_id: Optional[str] = None,
73
+ idempotency_key: Optional[str] = None,
74
+ ) -> Dict[str, Any]:
75
+ """POST /agent/v1/contact - submit a contact enquiry on behalf of a user.
76
+
77
+ A fresh Idempotency-Key is generated unless you pass one; reuse the same
78
+ key when retrying so a timeout never produces a duplicate enquiry.
79
+ """
80
+ if topic not in TOPICS:
81
+ raise ValidationError(f"topic must be one of: {', '.join(TOPICS)}")
82
+ if len(message.strip()) < 10:
83
+ raise ValidationError("message must be at least 10 characters")
84
+
85
+ body: Dict[str, Any] = {
86
+ "fromName": from_name,
87
+ "fromEmail": from_email,
88
+ "message": message,
89
+ "topic": topic,
90
+ }
91
+ if organization is not None:
92
+ body["organization"] = organization
93
+ if agent_id is not None:
94
+ body["agentId"] = agent_id
95
+ return self._request(
96
+ "POST",
97
+ f"{self._prefix}/contact",
98
+ body=body,
99
+ headers={"Idempotency-Key": idempotency_key or str(uuid.uuid4())},
100
+ )
101
+
102
+ def openapi(self) -> Dict[str, Any]:
103
+ """The OpenAPI 3.1 spec, parsed."""
104
+ return self._request("GET", "/agent/openapi.json")
105
+
106
+ def llms_txt(self) -> str:
107
+ """/llms.txt - the site's page index, as markdown text."""
108
+ return self._request("GET", "/llms.txt", raw=True)
109
+
110
+ def page(self, slug: str) -> str:
111
+ """Any page's markdown mirror, e.g. page("about") or page("case-studies/cloudflare-enterprise")."""
112
+ clean = re.sub(r"\.md$", "", slug.lstrip("/"))
113
+ if not clean:
114
+ raise ValidationError("slug is required")
115
+ return self._request("GET", f"/markdown/{clean}.md", raw=True)
116
+
117
+ def _request(
118
+ self,
119
+ method: str,
120
+ path: str,
121
+ query: Optional[Dict[str, Any]] = None,
122
+ body: Optional[Dict[str, Any]] = None,
123
+ headers: Optional[Dict[str, str]] = None,
124
+ raw: bool = False,
125
+ ) -> Any:
126
+ url = f"{self.base_url}{path}"
127
+ if query:
128
+ url = f"{url}?{urllib.parse.urlencode(query)}"
129
+
130
+ req = urllib.request.Request(url, method=method)
131
+ req.add_header("User-Agent", f"vertodigital-python/{__version__}")
132
+ req.add_header("Accept", "text/markdown, text/plain" if raw else "application/json")
133
+ for key, value in (headers or {}).items():
134
+ req.add_header(key, value)
135
+ data = None
136
+ if body is not None:
137
+ req.add_header("Content-Type", "application/json")
138
+ data = json.dumps(body).encode("utf-8")
139
+
140
+ try:
141
+ with urllib.request.urlopen(req, data=data, timeout=self.timeout) as res:
142
+ text = res.read().decode("utf-8")
143
+ except urllib.error.HTTPError as err:
144
+ text = err.read().decode("utf-8", errors="replace")
145
+ try:
146
+ parsed: Any = json.loads(text)
147
+ except ValueError:
148
+ parsed = text
149
+ if err.code == 429:
150
+ retry_after = err.headers.get("Retry-After")
151
+ raise RateLimitError(
152
+ err.code, parsed, path, int(retry_after) if retry_after and retry_after.isdigit() else None
153
+ ) from None
154
+ raise APIError(err.code, parsed, path) from None
155
+
156
+ if raw:
157
+ return text
158
+ return json.loads(text) if text else {}
@@ -0,0 +1,40 @@
1
+ """Exceptions raised by the vertodigital client."""
2
+
3
+ from typing import Any, Optional
4
+
5
+
6
+ class VertoDigitalError(Exception):
7
+ """Base class for everything this package raises."""
8
+
9
+
10
+ class ValidationError(VertoDigitalError, ValueError):
11
+ """An argument failed validation before any request was sent."""
12
+
13
+
14
+ class APIError(VertoDigitalError):
15
+ """Any non-2xx response.
16
+
17
+ ``status`` is the HTTP status code; ``body`` is the parsed JSON error body
18
+ when there is one, otherwise the raw text.
19
+ """
20
+
21
+ def __init__(self, status: int, body: Any, path: str) -> None:
22
+ self.status = status
23
+ self.body = body
24
+ self.path = path
25
+ detail = ""
26
+ if isinstance(body, dict):
27
+ message = body.get("error") or body.get("message")
28
+ if isinstance(message, dict):
29
+ message = message.get("message", message)
30
+ if message:
31
+ detail = f": {message}"
32
+ super().__init__(f"{status} from {path}{detail}")
33
+
34
+
35
+ class RateLimitError(APIError):
36
+ """A 429 response. ``retry_after`` is the wait in seconds, when the API sends Retry-After."""
37
+
38
+ def __init__(self, status: int, body: Any, path: str, retry_after: Optional[int]) -> None:
39
+ self.retry_after = retry_after
40
+ super().__init__(status, body, path)
File without changes
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: vertodigital
3
+ Version: 0.2.0
4
+ Summary: Python client for VertoDigital's public agent API.
5
+ Author-email: VertoDigital <team@vertodigital.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://vertodigital.com
8
+ Project-URL: Documentation, https://vertodigital.com/developers
9
+ Project-URL: Changelog, https://pypi.org/project/vertodigital/#history
10
+ Keywords: vertodigital,api,sdk,agent,mcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.txt
24
+ Dynamic: license-file
25
+
26
+ # vertodigital
27
+
28
+ Official Python client for [VertoDigital's public agent API](https://vertodigital.com/developers). Every endpoint is anonymous, so there's no API key to set up. No runtime dependencies, just the Python standard library.
29
+
30
+ ## Install
31
+
32
+ ```
33
+ pip install vertodigital
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from vertodigital import Client
40
+
41
+ client = Client()
42
+
43
+ client.health()
44
+ # {'status': 'ok', 'service': 'vertodigital-agent-api', ...}
45
+
46
+ # One page of case studies
47
+ page = client.case_studies(limit=10)
48
+ for cs in page["items"]:
49
+ print(f'{cs["name"]}: {cs["stat"]}')
50
+
51
+ # Or every case study, paginated for you
52
+ urls = [cs["url"] for cs in client.iter_case_studies()]
53
+
54
+ # Any page on vertodigital.com as markdown
55
+ print(client.page("about"))
56
+ print(client.llms_txt())
57
+
58
+ # The OpenAPI 3.1 spec
59
+ list(client.openapi()["paths"])
60
+
61
+ # Submit a contact enquiry on behalf of a user
62
+ client.contact(
63
+ from_name="Ada Lovelace",
64
+ from_email="ada@example.com",
65
+ organization="Analytical Engines Ltd",
66
+ topic="services", # assessment, services, partnership, press, careers, general
67
+ message="We'd like to discuss a marketing analytics engagement.",
68
+ )
69
+ ```
70
+
71
+ `contact` sends an `Idempotency-Key` automatically. If you retry after a timeout, pass the same `idempotency_key=` again so the enquiry is only recorded once.
72
+
73
+ Requests go to the pinned `/agent/v1/*` paths by default. Pass `api_version=None` to use the unversioned `/agent/*` paths.
74
+
75
+ ## Errors
76
+
77
+ - `vertodigital.APIError` for any non-2xx response. It carries `status` and `body`.
78
+ - `vertodigital.RateLimitError` (a subclass) on 429. It adds `retry_after` in seconds.
79
+ - `vertodigital.ValidationError` when input fails validation before a request is sent.
80
+
81
+ All three inherit from `vertodigital.VertoDigitalError`. Rate limits and the full API reference are at [vertodigital.com/developers](https://vertodigital.com/developers).
82
+
83
+ ## Changelog
84
+
85
+ - **0.2.0** - Requires Python 3.11 or later (3.8, 3.9 and 3.10 have reached end of life). No API changes.
86
+ - **0.1.0** - First release: `health`, `case_studies` / `iter_case_studies`, `contact`, `openapi`, `llms_txt`, `page`.
87
+
88
+ ## Publishing (maintainers)
89
+
90
+ From this directory:
91
+
92
+ ```
93
+ python3 -m pip install --upgrade build twine
94
+ python3 -m build
95
+ python3 -m twine upload dist/*
96
+ ```
97
+
98
+ Bump `__version__` in `src/vertodigital/_version.py` and add a changelog entry on every release. Upload with a PyPI API token (username `__token__`).
@@ -0,0 +1,12 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ src/vertodigital/__init__.py
5
+ src/vertodigital/_version.py
6
+ src/vertodigital/client.py
7
+ src/vertodigital/errors.py
8
+ src/vertodigital/py.typed
9
+ src/vertodigital.egg-info/PKG-INFO
10
+ src/vertodigital.egg-info/SOURCES.txt
11
+ src/vertodigital.egg-info/dependency_links.txt
12
+ src/vertodigital.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ vertodigital