Skip to content
Merged

V3 #18

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7ff18bc
Start v3
lemonyte Aug 21, 2025
d4ae1b9
Remove x-api-key header
lemonyte Aug 21, 2025
9f928ee
Create base product class
lemonyte Aug 21, 2025
3bc627a
Add IM impl
lemonyte Aug 21, 2025
e0b5563
Simplify error and debug messages
lemonyte Aug 21, 2025
2b2cd0e
Properly handle API response format
lemonyte Aug 21, 2025
035c3c8
Use logger instead of print
lemonyte Aug 21, 2025
fd84c1e
Fix filename typo
lemonyte Aug 21, 2025
b97ac59
Add domains impl
lemonyte Aug 23, 2025
dd54232
re-use api error handling
lemonyte Aug 23, 2025
af40d81
update email overloads
lemonyte Aug 23, 2025
0af2417
Forgot these changes
lemonyte Sep 18, 2025
7312269
Add lease impl
lemonyte Sep 18, 2025
3b27446
Add leases and domains to contiguity class
lemonyte Sep 18, 2025
9041278
fix lint
lemonyte Sep 18, 2025
9465bc2
Update base examples and tests for msgspec
lemonyte Sep 18, 2025
62e606e
Update dev deps
lemonyte Oct 6, 2025
5b7241f
Bump phonenumbers to v9
lemonyte Oct 6, 2025
9458053
add imessage read and history endpoints
Oct 13, 2025
e91b586
Bump minimum python version to 3.10
Oct 13, 2025
8bdd445
Remove usage of future annotations
Oct 13, 2025
cb4fedb
Add from and attachments params to text.send
Oct 13, 2025
efd9a04
fix status code check
lemonyte Oct 14, 2025
dc45307
handle unexpected objects in response data
lemonyte Oct 14, 2025
34d280e
fix overload typing
lemonyte Oct 14, 2025
47dafda
Remove pydantic mentions
lemonyte Oct 21, 2025
d6b5078
Fix test discovery
lemonyte Oct 21, 2025
fe95248
Fix item typing in Base
lemonyte Oct 21, 2025
665efef
Refactor: Use response_type in decode_response
cursoragent Oct 22, 2025
93483e4
Revert "AI nonsense"
lemonyte Oct 22, 2025
06a4719
pre-commit autoupdate
lemonyte Oct 22, 2025
67da89f
Bump python version in workflows
lemonyte Oct 22, 2025
cb26916
Indicate identity is not implemented
lemonyte Oct 22, 2025
36a5d89
Rename email.email to email.send
lemonyte Oct 22, 2025
4baa451
Make token param optional
lemonyte Oct 22, 2025
6cb9102
Ignore assert lint in tests
lemonyte Oct 22, 2025
0350b6a
Clean up tests
lemonyte Oct 22, 2025
d985620
Add preliminary sdk tests
lemonyte Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
lint:
runs-on: ubuntu-latest
env:
PYTHON_VERSION: "3.9"
PYTHON_VERSION: "3.10"
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
test:
runs-on: ubuntu-latest
env:
PYTHON_VERSION: "3.9"
PYTHON_VERSION: "3.10"
CONTIGUITY_PROJECT_ID: ${{ secrets.CONTIGUITY_PROJECT_ID }}
CONTIGUITY_TOKEN: ${{ secrets.CONTIGUITY_TOKEN }}
CONTIGUITY_DATA_KEY: ${{ secrets.CONTIGUITY_DATA_KEY }}
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
Expand All @@ -11,12 +11,12 @@ repos:
args: [--markdown-linebreak-ext=md]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
rev: v0.14.1
hooks:
- id: ruff
- id: ruff-format

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.384
rev: v1.1.406
hooks:
- id: pyright
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ import contiguity
client = contiguity.login("your_token_here")
```

You can also initialize it with the optional 'debug' flag:

```js
client = contiguity.login("your_token_here", True)
```

You can get your token from the Contiguity [dashboard](https://contiguity.co/dashboard).

## Sending your first email 📤
Expand Down
3 changes: 1 addition & 2 deletions examples/base/basic_usage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# ruff: noqa: T201
from contiguity import Base
from contiguity.base import Base

# Create a Base instance.
db = Base("my-base")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
# ruff: noqa: T201
from pydantic import BaseModel
from msgspec import Struct

from contiguity import Base
from contiguity.base import Base


# Create a Pydantic model for the item.
class MyItem(BaseModel):
# Create a msgspec model for the item.
class MyItem(Struct):
key: str # Make sure to include the key field.
name: str
age: int
interests: list[str] = []


# Create a Base instance.
# Static type checking will work with the Pydantic model.
# Static type checking will work with the msgspec model.
db = Base("members", item_type=MyItem)

# Put an item with a specific key.
Expand Down
21 changes: 12 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dynamic = ["version"]
description = "Contiguity's official Python SDK"
readme = "README.md"
license = {file = "LICENSE.txt"}
requires-python = ">=3.9"
requires-python = ">=3.10"
authors = [
{name = "Contiguity", email = "help@contiguity.support"},
]
Expand All @@ -35,18 +35,18 @@ classifiers = [
]
dependencies = [
"httpx>=0.27.2",
"phonenumbers>=8.13.47,<9.0.0",
"pydantic>=2.9.0,<3.0.0",
"msgspec>=0.19.0",
"phonenumbers>=9.0.15,<10.0.0",
"typing-extensions>=4.12.2,<5.0.0",
]

[dependency-groups]
dev = [
"pre-commit~=3.8.0",
"pytest~=8.3.3",
"pytest-cov~=5.0.0",
"python-dotenv~=1.0.1",
"pytest-asyncio~=0.24.0",
"pre-commit~=4.3.0",
"pytest~=8.4.2",
"pytest-asyncio~=1.2.0",
"pytest-cov~=7.0.0",
"python-dotenv~=1.1.1",
]

[project.urls]
Expand All @@ -61,12 +61,15 @@ version = {attr = "contiguity.__version__"}
[tool.ruff]
src = ["src"]
line-length = 119
target-version = "py39"
target-version = "py310"

[tool.ruff.lint]
select = ["ALL"]
ignore = ["A", "D", "T201"]

[tool.ruff.lint.per-file-ignores]
"**/tests/*" = ["S101", "S311"]

[tool.pyright]
venvPath = "."
venv = ".venv"
Expand Down
76 changes: 27 additions & 49 deletions src/contiguity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,44 @@
from ._auth import get_contiguity_token
from ._client import ApiClient
from .analytics import EmailAnalytics
from .base import AsyncBase, Base, BaseItem, InvalidKeyError, ItemConflictError, ItemNotFoundError, QueryResponse
from .domains import Domains
from .email import Email
from .imessage import IMessage
from .leases import Leases
from .otp import OTP
from .quota import Quota
from .send import Send
from .template import Template
from .verify import Verify
from .text import Text
from .whatsapp import WhatsApp


class Contiguity:
"""
Create a new instance of the Contiguity class.
Args:
token (str): The authentication token.
debug (bool, optional): A flag indicating whether to enable debug mode. Default is False.
"""
"""The Contiguity client."""

def __init__(
self,
*,
token: str,
base_url: str = "https://api.contiguity.co",
orwell_base_url: str = "https://orwell.contiguity.co",
debug: bool = False,
token: str | None = None,
base_url: str = "https://api.contiguity.com",
) -> None:
if not token:
msg = "Contiguity requires a token/API key to be provided via contiguity.login('token')"
raise ValueError(msg)
self.token = token
self.token = token or get_contiguity_token()
self.base_url = base_url
self.orwell_base_url = orwell_base_url
self.debug = debug
self.client = ApiClient(base_url=self.base_url, api_key=token.strip())
self.orwell_client = ApiClient(base_url=self.orwell_base_url, api_key=token.strip())
self.client = ApiClient(base_url=self.base_url, api_key=self.token.strip())

self.send = Send(client=self.client, debug=self.debug)
self.verify = Verify()
self.email_analytics = EmailAnalytics(client=self.orwell_client, debug=self.debug)
self.quota = Quota(client=self.client, debug=self.debug)
self.otp = OTP(client=self.client, debug=self.debug)
self.template = Template()


def login(token: str, /, *, debug: bool = False) -> Contiguity:
return Contiguity(token=token, debug=debug)
self.text = Text(client=self.client)
self.email = Email(client=self.client)
self.otp = OTP(client=self.client)
self.imessage = IMessage(client=self.client)
self.whatsapp = WhatsApp(client=self.client)
self.leases = Leases(client=self.client)
self.domains = Domains(client=self.client)


__all__ = (
"AsyncBase",
"Contiguity",
"Send",
"Verify",
"EmailAnalytics",
"Quota",
"OTP",
"Template",
"Base",
"BaseItem",
"InvalidKeyError",
"ItemConflictError",
"ItemNotFoundError",
"QueryResponse",
"login",
"Contiguity",
"Domains",
"Email",
"IMessage",
"Leases",
"Text",
"WhatsApp",
)
__version__ = "2.0.0"
__version__ = "3.0.0"
2 changes: 0 additions & 2 deletions src/contiguity/_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

import os


Expand Down
28 changes: 18 additions & 10 deletions src/contiguity/_client.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
from __future__ import annotations
from http import HTTPStatus

from httpx import AsyncClient as HttpxAsyncClient
from httpx import Client as HttpxClient
from httpx import Response

from ._auth import get_contiguity_token
from ._response import ErrorResponse, decode_response


class ApiError(Exception):
class ContiguityApiError(Exception):
pass


class ApiClient(HttpxClient):
class BaseApiClient:
def handle_error(self, response: Response, /, *, fail_message: str = "api request failed") -> None:
if not HTTPStatus.OK <= response.status_code < HTTPStatus.MULTIPLE_CHOICES:
data = decode_response(response.content, type=ErrorResponse)
msg = f"{fail_message}. {response.status_code} {data.error}"
raise ContiguityApiError(msg)


class ApiClient(HttpxClient, BaseApiClient):
def __init__(
self: ApiClient,
self: "ApiClient",
*,
base_url: str = "https://api.contiguity.co",
base_url: str = "https://api.contiguity.com",
api_key: str | None = None,
timeout: int = 5,
) -> None:
Expand All @@ -23,19 +33,18 @@ def __init__(
super().__init__(
headers={
"Content-Type": "application/json",
"X-API-Key": api_key,
"Authorization": f"Token {api_key}",
},
timeout=timeout,
base_url=base_url,
)


class AsyncApiClient(HttpxAsyncClient):
class AsyncApiClient(HttpxAsyncClient, BaseApiClient):
def __init__(
self: AsyncApiClient,
self: "AsyncApiClient",
*,
base_url: str = "https://api.contiguity.co",
base_url: str = "https://api.contiguity.com",
api_key: str | None = None,
timeout: int = 5,
) -> None:
Expand All @@ -44,7 +53,6 @@ def __init__(
super().__init__(
headers={
"Content-Type": "application/json",
"X-API-Key": api_key,
"Authorization": f"Token {api_key}",
},
timeout=timeout,
Expand Down
8 changes: 0 additions & 8 deletions src/contiguity/_common.py

This file was deleted.

Loading
Loading