crosspoint 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: crosspoint
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Crosspoint API client: the Polar SDK, pointed at Crosspoint
5
5
  Project-URL: Documentation, https://app.crosspoint.finance/docs/africa/sdks
6
6
  License-Expression: MIT
@@ -44,4 +44,7 @@ except WebhookVerificationError:
44
44
  ... # not from Crosspoint: reject it
45
45
  ```
46
46
 
47
+ Sandbox (test cards, no real money): set `CROSSPOINT_ENVIRONMENT=sandbox` with a sandbox
48
+ access token, or `Crosspoint(server="sandbox")`.
49
+
47
50
  Docs: https://app.crosspoint.finance/docs/africa/sdks
@@ -34,4 +34,7 @@ except WebhookVerificationError:
34
34
  ... # not from Crosspoint: reject it
35
35
  ```
36
36
 
37
+ Sandbox (test cards, no real money): set `CROSSPOINT_ENVIRONMENT=sandbox` with a sandbox
38
+ access token, or `Crosspoint(server="sandbox")`.
39
+
37
40
  Docs: https://app.crosspoint.finance/docs/africa/sdks
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crosspoint"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Crosspoint API client: the Polar SDK, pointed at Crosspoint"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,24 @@
1
+ """Crosspoint API client: the Polar SDK, pointed at Crosspoint.
2
+
3
+ The default API version is 2026-10; pin one with ``from crosspoint.v2026_10 import ...``.
4
+ """
5
+
6
+ from . import webhooks
7
+ from .v2026_10 import (
8
+ CROSSPOINT_API_URL,
9
+ CROSSPOINT_SANDBOX_API_URL,
10
+ SPLIT_METADATA_KEY,
11
+ CrosspointAPIError,
12
+ Crosspoint,
13
+ CrosspointAsync,
14
+ )
15
+
16
+ __all__ = [
17
+ "CROSSPOINT_API_URL",
18
+ "CROSSPOINT_SANDBOX_API_URL",
19
+ "SPLIT_METADATA_KEY",
20
+ "Crosspoint",
21
+ "CrosspointAPIError",
22
+ "CrosspointAsync",
23
+ "webhooks",
24
+ ]
@@ -0,0 +1,38 @@
1
+ """Crosspoint's API is Polar's API: the Polar SDK works once it points at Crosspoint."""
2
+
3
+ import os
4
+ from typing import Literal
5
+
6
+ CROSSPOINT_API_URL = "https://api.crosspoint.finance"
7
+ CROSSPOINT_SANDBOX_API_URL = "https://sandbox-api.crosspoint.finance"
8
+ ACCESS_TOKEN_ENV = "CROSSPOINT_ACCESS_TOKEN"
9
+ ENVIRONMENT_ENV = "CROSSPOINT_ENVIRONMENT"
10
+
11
+ # "sandbox" is Crosspoint's test environment: test cards, no real money.
12
+ Server = Literal["production", "sandbox"]
13
+ API_URLS: dict[str, str] = {
14
+ "production": CROSSPOINT_API_URL,
15
+ "sandbox": CROSSPOINT_SANDBOX_API_URL,
16
+ }
17
+
18
+
19
+ def resolve_access_token(access_token: str | None) -> str:
20
+ token = access_token or os.environ.get(ACCESS_TOKEN_ENV)
21
+ if not token:
22
+ raise ValueError(
23
+ f"Crosspoint: pass access_token or set {ACCESS_TOKEN_ENV} "
24
+ "(an organization access token, cxp_oat_...)."
25
+ )
26
+ return token
27
+
28
+
29
+ def resolve_base_url(base_url: str | None, server: str | None) -> str:
30
+ """`base_url` wins; else `server`, then CROSSPOINT_ENVIRONMENT, then production."""
31
+ if base_url:
32
+ return base_url
33
+ server = server or os.environ.get(ENVIRONMENT_ENV) or "production"
34
+ if server not in API_URLS:
35
+ raise ValueError(
36
+ f'Crosspoint: server must be "production" or "sandbox", not "{server}".'
37
+ )
38
+ return API_URLS[server]
@@ -0,0 +1,162 @@
1
+ """Transaction splits (/v1/splits, /v1/split-recipients): Crosspoint's own endpoints,
2
+ so the Polar SDK doesn't have them. Requests and responses are plain dicts, with the
3
+ API's field names."""
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ # Put a split's ID here in a checkout's (or checkout link's) metadata to use it.
11
+ SPLIT_METADATA_KEY = "crosspoint_split"
12
+
13
+ JSON = dict[str, Any]
14
+
15
+
16
+ class CrosspointAPIError(Exception):
17
+ """A non-2xx answer from the API; `body` has the details (e.g. validation errors)."""
18
+
19
+ def __init__(self, status_code: int, body: Any) -> None:
20
+ super().__init__(f"Crosspoint API error {status_code}: {body}")
21
+ self.status_code = status_code
22
+ self.body = body
23
+
24
+
25
+ def _answer(response: httpx.Response) -> Any:
26
+ data = response.json() if response.content else None
27
+ if response.is_error:
28
+ raise CrosspointAPIError(response.status_code, data)
29
+ return data
30
+
31
+
32
+ def _params(
33
+ organization_id: str | Sequence[str] | None,
34
+ page: int | None,
35
+ limit: int | None,
36
+ **extra: Any,
37
+ ) -> list[tuple[str, str]]:
38
+ ids = [organization_id] if isinstance(organization_id, str) else organization_id or []
39
+ params = [("organization_id", str(id)) for id in ids]
40
+ for key, value in {"page": page, "limit": limit, **extra}.items():
41
+ if value is not None:
42
+ params.append((key, str(value)))
43
+ return params
44
+
45
+
46
+ def _client(cls: type, access_token: str, base_url: str, timeout: Any) -> Any:
47
+ return cls(
48
+ base_url=base_url,
49
+ headers={"Authorization": f"Bearer {access_token}", "Accept": "application/json"},
50
+ timeout=timeout,
51
+ )
52
+
53
+
54
+ class _Resource:
55
+ def __init__(self, http: httpx.Client, path: str) -> None:
56
+ self._http = http
57
+ self._path = path
58
+
59
+ def get(self, id: str) -> JSON:
60
+ return _answer(self._http.get(f"{self._path}/{id}"))
61
+
62
+ def create(self, body: JSON) -> JSON:
63
+ return _answer(self._http.post(f"{self._path}/", json=body))
64
+
65
+ def update(self, id: str, body: JSON) -> JSON:
66
+ return _answer(self._http.patch(f"{self._path}/{id}", json=body))
67
+
68
+ def delete(self, id: str) -> None:
69
+ _answer(self._http.delete(f"{self._path}/{id}"))
70
+
71
+
72
+ class Splits(_Resource):
73
+ def __init__(self, http: httpx.Client) -> None:
74
+ super().__init__(http, "/v1/splits")
75
+
76
+ def list(
77
+ self,
78
+ *,
79
+ organization_id: str | Sequence[str] | None = None,
80
+ page: int | None = None,
81
+ limit: int | None = None,
82
+ ) -> JSON:
83
+ params = _params(organization_id, page, limit)
84
+ return _answer(self._http.get(f"{self._path}/", params=params))
85
+
86
+
87
+ class SplitRecipients(_Resource):
88
+ def __init__(self, http: httpx.Client) -> None:
89
+ super().__init__(http, "/v1/split-recipients")
90
+
91
+ def list(
92
+ self,
93
+ *,
94
+ organization_id: str | Sequence[str] | None = None,
95
+ status: str | None = None,
96
+ page: int | None = None,
97
+ limit: int | None = None,
98
+ ) -> JSON:
99
+ params = _params(organization_id, page, limit, status=status)
100
+ return _answer(self._http.get(f"{self._path}/", params=params))
101
+
102
+
103
+ class _AsyncResource:
104
+ def __init__(self, http: httpx.AsyncClient, path: str) -> None:
105
+ self._http = http
106
+ self._path = path
107
+
108
+ async def get(self, id: str) -> JSON:
109
+ return _answer(await self._http.get(f"{self._path}/{id}"))
110
+
111
+ async def create(self, body: JSON) -> JSON:
112
+ return _answer(await self._http.post(f"{self._path}/", json=body))
113
+
114
+ async def update(self, id: str, body: JSON) -> JSON:
115
+ return _answer(await self._http.patch(f"{self._path}/{id}", json=body))
116
+
117
+ async def delete(self, id: str) -> None:
118
+ _answer(await self._http.delete(f"{self._path}/{id}"))
119
+
120
+
121
+ class AsyncSplits(_AsyncResource):
122
+ def __init__(self, http: httpx.AsyncClient) -> None:
123
+ super().__init__(http, "/v1/splits")
124
+
125
+ async def list(
126
+ self,
127
+ *,
128
+ organization_id: str | Sequence[str] | None = None,
129
+ page: int | None = None,
130
+ limit: int | None = None,
131
+ ) -> JSON:
132
+ params = _params(organization_id, page, limit)
133
+ return _answer(await self._http.get(f"{self._path}/", params=params))
134
+
135
+
136
+ class AsyncSplitRecipients(_AsyncResource):
137
+ def __init__(self, http: httpx.AsyncClient) -> None:
138
+ super().__init__(http, "/v1/split-recipients")
139
+
140
+ async def list(
141
+ self,
142
+ *,
143
+ organization_id: str | Sequence[str] | None = None,
144
+ status: str | None = None,
145
+ page: int | None = None,
146
+ limit: int | None = None,
147
+ ) -> JSON:
148
+ params = _params(organization_id, page, limit, status=status)
149
+ return _answer(await self._http.get(f"{self._path}/", params=params))
150
+
151
+
152
+ def attach(client: Any, access_token: str, base_url: str, timeout: Any) -> None:
153
+ """`client.splits` and `client.split_recipients` on a sync client."""
154
+ http = _client(httpx.Client, access_token, base_url, timeout)
155
+ client.splits = Splits(http)
156
+ client.split_recipients = SplitRecipients(http)
157
+
158
+
159
+ def attach_async(client: Any, access_token: str, base_url: str, timeout: Any) -> None:
160
+ http = _client(httpx.AsyncClient, access_token, base_url, timeout)
161
+ client.splits = AsyncSplits(http)
162
+ client.split_recipients = AsyncSplitRecipients(http)
@@ -0,0 +1,77 @@
1
+ """Crosspoint client for API version 2026_04."""
2
+
3
+ from typing import Any
4
+
5
+ from polar.v2026_04 import Polar, PolarAsync
6
+
7
+ from .._options import (
8
+ CROSSPOINT_API_URL,
9
+ CROSSPOINT_SANDBOX_API_URL,
10
+ Server,
11
+ resolve_access_token,
12
+ resolve_base_url,
13
+ )
14
+ from .._splits import (
15
+ SPLIT_METADATA_KEY,
16
+ AsyncSplitRecipients,
17
+ AsyncSplits,
18
+ CrosspointAPIError,
19
+ SplitRecipients,
20
+ Splits,
21
+ attach,
22
+ attach_async,
23
+ )
24
+ from . import webhooks
25
+
26
+ __all__ = [
27
+ "CROSSPOINT_API_URL",
28
+ "CROSSPOINT_SANDBOX_API_URL",
29
+ "SPLIT_METADATA_KEY",
30
+ "CrosspointAPIError",
31
+ "Crosspoint",
32
+ "CrosspointAsync",
33
+ "webhooks",
34
+ ]
35
+
36
+
37
+ class Crosspoint(Polar):
38
+ """
39
+ `Crosspoint()` reads CROSSPOINT_ACCESS_TOKEN. `server="sandbox"` (or
40
+ CROSSPOINT_ENVIRONMENT=sandbox) uses the sandbox; `base_url` is for local development.
41
+ """
42
+
43
+ splits: Splits
44
+ split_recipients: SplitRecipients
45
+
46
+ def __init__(
47
+ self,
48
+ access_token: str | None = None,
49
+ *,
50
+ server: Server | None = None,
51
+ base_url: str | None = None,
52
+ timeout: Any = 5.0,
53
+ ) -> None:
54
+ token = resolve_access_token(access_token)
55
+ url = resolve_base_url(base_url, server)
56
+ super().__init__(token, base_url=url, timeout=timeout)
57
+ attach(self, token, url, timeout)
58
+
59
+
60
+ class CrosspointAsync(PolarAsync):
61
+ """Async client: `async with CrosspointAsync() as crosspoint: ...`."""
62
+
63
+ splits: AsyncSplits
64
+ split_recipients: AsyncSplitRecipients
65
+
66
+ def __init__(
67
+ self,
68
+ access_token: str | None = None,
69
+ *,
70
+ server: Server | None = None,
71
+ base_url: str | None = None,
72
+ timeout: Any = 5.0,
73
+ ) -> None:
74
+ token = resolve_access_token(access_token)
75
+ url = resolve_base_url(base_url, server)
76
+ super().__init__(token, base_url=url, timeout=timeout)
77
+ attach_async(self, token, url, timeout)
@@ -0,0 +1,77 @@
1
+ """Crosspoint client for API version 2026_10."""
2
+
3
+ from typing import Any
4
+
5
+ from polar.v2026_10 import Polar, PolarAsync
6
+
7
+ from .._options import (
8
+ CROSSPOINT_API_URL,
9
+ CROSSPOINT_SANDBOX_API_URL,
10
+ Server,
11
+ resolve_access_token,
12
+ resolve_base_url,
13
+ )
14
+ from .._splits import (
15
+ SPLIT_METADATA_KEY,
16
+ AsyncSplitRecipients,
17
+ AsyncSplits,
18
+ CrosspointAPIError,
19
+ SplitRecipients,
20
+ Splits,
21
+ attach,
22
+ attach_async,
23
+ )
24
+ from . import webhooks
25
+
26
+ __all__ = [
27
+ "CROSSPOINT_API_URL",
28
+ "CROSSPOINT_SANDBOX_API_URL",
29
+ "SPLIT_METADATA_KEY",
30
+ "CrosspointAPIError",
31
+ "Crosspoint",
32
+ "CrosspointAsync",
33
+ "webhooks",
34
+ ]
35
+
36
+
37
+ class Crosspoint(Polar):
38
+ """
39
+ `Crosspoint()` reads CROSSPOINT_ACCESS_TOKEN. `server="sandbox"` (or
40
+ CROSSPOINT_ENVIRONMENT=sandbox) uses the sandbox; `base_url` is for local development.
41
+ """
42
+
43
+ splits: Splits
44
+ split_recipients: SplitRecipients
45
+
46
+ def __init__(
47
+ self,
48
+ access_token: str | None = None,
49
+ *,
50
+ server: Server | None = None,
51
+ base_url: str | None = None,
52
+ timeout: Any = 5.0,
53
+ ) -> None:
54
+ token = resolve_access_token(access_token)
55
+ url = resolve_base_url(base_url, server)
56
+ super().__init__(token, base_url=url, timeout=timeout)
57
+ attach(self, token, url, timeout)
58
+
59
+
60
+ class CrosspointAsync(PolarAsync):
61
+ """Async client: `async with CrosspointAsync() as crosspoint: ...`."""
62
+
63
+ splits: AsyncSplits
64
+ split_recipients: AsyncSplitRecipients
65
+
66
+ def __init__(
67
+ self,
68
+ access_token: str | None = None,
69
+ *,
70
+ server: Server | None = None,
71
+ base_url: str | None = None,
72
+ timeout: Any = 5.0,
73
+ ) -> None:
74
+ token = resolve_access_token(access_token)
75
+ url = resolve_base_url(base_url, server)
76
+ super().__init__(token, base_url=url, timeout=timeout)
77
+ attach_async(self, token, url, timeout)
@@ -0,0 +1,77 @@
1
+ """Crosspoint client for API version 2027_01."""
2
+
3
+ from typing import Any
4
+
5
+ from polar.v2027_01 import Polar, PolarAsync
6
+
7
+ from .._options import (
8
+ CROSSPOINT_API_URL,
9
+ CROSSPOINT_SANDBOX_API_URL,
10
+ Server,
11
+ resolve_access_token,
12
+ resolve_base_url,
13
+ )
14
+ from .._splits import (
15
+ SPLIT_METADATA_KEY,
16
+ AsyncSplitRecipients,
17
+ AsyncSplits,
18
+ CrosspointAPIError,
19
+ SplitRecipients,
20
+ Splits,
21
+ attach,
22
+ attach_async,
23
+ )
24
+ from . import webhooks
25
+
26
+ __all__ = [
27
+ "CROSSPOINT_API_URL",
28
+ "CROSSPOINT_SANDBOX_API_URL",
29
+ "SPLIT_METADATA_KEY",
30
+ "CrosspointAPIError",
31
+ "Crosspoint",
32
+ "CrosspointAsync",
33
+ "webhooks",
34
+ ]
35
+
36
+
37
+ class Crosspoint(Polar):
38
+ """
39
+ `Crosspoint()` reads CROSSPOINT_ACCESS_TOKEN. `server="sandbox"` (or
40
+ CROSSPOINT_ENVIRONMENT=sandbox) uses the sandbox; `base_url` is for local development.
41
+ """
42
+
43
+ splits: Splits
44
+ split_recipients: SplitRecipients
45
+
46
+ def __init__(
47
+ self,
48
+ access_token: str | None = None,
49
+ *,
50
+ server: Server | None = None,
51
+ base_url: str | None = None,
52
+ timeout: Any = 5.0,
53
+ ) -> None:
54
+ token = resolve_access_token(access_token)
55
+ url = resolve_base_url(base_url, server)
56
+ super().__init__(token, base_url=url, timeout=timeout)
57
+ attach(self, token, url, timeout)
58
+
59
+
60
+ class CrosspointAsync(PolarAsync):
61
+ """Async client: `async with CrosspointAsync() as crosspoint: ...`."""
62
+
63
+ splits: AsyncSplits
64
+ split_recipients: AsyncSplitRecipients
65
+
66
+ def __init__(
67
+ self,
68
+ access_token: str | None = None,
69
+ *,
70
+ server: Server | None = None,
71
+ base_url: str | None = None,
72
+ timeout: Any = 5.0,
73
+ ) -> None:
74
+ token = resolve_access_token(access_token)
75
+ url = resolve_base_url(base_url, server)
76
+ super().__init__(token, base_url=url, timeout=timeout)
77
+ attach_async(self, token, url, timeout)
@@ -0,0 +1,126 @@
1
+ import httpx
2
+ import pytest
3
+
4
+ from crosspoint import CROSSPOINT_API_URL, Crosspoint
5
+ from crosspoint.v2026_04 import Crosspoint as Crosspoint202604
6
+
7
+
8
+ def http(client: Crosspoint) -> httpx.Client:
9
+ return client._client._client # the SDK's httpx client
10
+
11
+
12
+ def test_talks_to_crosspoint_not_polar() -> None:
13
+ client = http(Crosspoint("cxp_oat_test"))
14
+
15
+ assert str(client.base_url).rstrip("/") == CROSSPOINT_API_URL
16
+ assert client.headers["authorization"] == "Bearer cxp_oat_test"
17
+
18
+
19
+ def test_local_development_url() -> None:
20
+ client = http(Crosspoint("cxp_oat_test", base_url="http://127.0.0.1:8001"))
21
+ assert str(client.base_url).rstrip("/") == "http://127.0.0.1:8001"
22
+
23
+
24
+ def test_reads_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
25
+ monkeypatch.setenv("CROSSPOINT_ACCESS_TOKEN", "cxp_oat_env")
26
+ Crosspoint()
27
+ Crosspoint202604()
28
+
29
+
30
+ def test_explains_a_missing_token(monkeypatch: pytest.MonkeyPatch) -> None:
31
+ monkeypatch.delenv("CROSSPOINT_ACCESS_TOKEN", raising=False)
32
+ with pytest.raises(ValueError, match="CROSSPOINT_ACCESS_TOKEN"):
33
+ Crosspoint()
34
+
35
+
36
+ def test_every_api_version(monkeypatch: pytest.MonkeyPatch) -> None:
37
+ from crosspoint.v2027_01 import Crosspoint as Crosspoint202701
38
+
39
+ client = http(Crosspoint202701("cxp_oat_test"))
40
+ assert str(client.base_url).rstrip("/") == CROSSPOINT_API_URL
41
+
42
+
43
+ def test_webhooks_import_paths() -> None:
44
+ import crosspoint
45
+ from crosspoint.v2027_01.webhooks import validate_event as pinned
46
+ from crosspoint.webhooks import (
47
+ PolarWebhookVerificationError,
48
+ WebhookUnknownTypeError,
49
+ WebhookVerificationError,
50
+ validate_event,
51
+ )
52
+
53
+ assert crosspoint.webhooks.validate_event is validate_event
54
+ assert callable(pinned)
55
+ assert WebhookVerificationError is PolarWebhookVerificationError
56
+ assert issubclass(WebhookUnknownTypeError, Exception)
57
+ with pytest.raises(WebhookVerificationError):
58
+ validate_event(
59
+ "{}",
60
+ {
61
+ "webhook-id": "x",
62
+ "webhook-timestamp": "1",
63
+ "webhook-signature": "v1,bad",
64
+ },
65
+ "secret",
66
+ )
67
+
68
+
69
+ def test_sandbox(monkeypatch: pytest.MonkeyPatch) -> None:
70
+ from crosspoint import CROSSPOINT_SANDBOX_API_URL
71
+
72
+ assert CROSSPOINT_SANDBOX_API_URL == "https://sandbox-api.crosspoint.finance"
73
+ client = http(Crosspoint("cxp_oat_test", server="sandbox"))
74
+ assert str(client.base_url).rstrip("/") == CROSSPOINT_SANDBOX_API_URL
75
+
76
+ monkeypatch.setenv("CROSSPOINT_ENVIRONMENT", "sandbox")
77
+ client = http(Crosspoint202604("cxp_oat_test"))
78
+ assert str(client.base_url).rstrip("/") == CROSSPOINT_SANDBOX_API_URL
79
+
80
+
81
+ def test_unknown_server() -> None:
82
+ with pytest.raises(ValueError, match="production"):
83
+ Crosspoint("cxp_oat_test", server="staging") # type: ignore[arg-type]
84
+
85
+
86
+ def test_splits() -> None:
87
+ from crosspoint import SPLIT_METADATA_KEY, CrosspointAPIError
88
+
89
+ calls: list[httpx.Request] = []
90
+
91
+ def answer(request: httpx.Request) -> httpx.Response:
92
+ calls.append(request)
93
+ if request.method == "GET":
94
+ return httpx.Response(422, json={"detail": [{"msg": "Bad status"}]})
95
+ return httpx.Response(201, json={"id": "spl_1", "fee_bearer": "shared"})
96
+
97
+ client = Crosspoint("cxp_oat_test", base_url="http://127.0.0.1:8001")
98
+ client.splits._http._transport = httpx.MockTransport(answer)
99
+
100
+ split = client.splits.create({"name": "Partners", "shares": []})
101
+ assert split == {"id": "spl_1", "fee_bearer": "shared"}
102
+ assert str(calls[0].url) == "http://127.0.0.1:8001/v1/splits/"
103
+ assert calls[0].headers["authorization"] == "Bearer cxp_oat_test"
104
+
105
+ client.split_recipients._http._transport = httpx.MockTransport(answer)
106
+ with pytest.raises(CrosspointAPIError) as error:
107
+ client.split_recipients.list(organization_id=["a", "b"], status="pending")
108
+ assert error.value.status_code == 422
109
+ assert str(calls[1].url) == (
110
+ "http://127.0.0.1:8001/v1/split-recipients/"
111
+ "?organization_id=a&organization_id=b&status=pending"
112
+ )
113
+ assert SPLIT_METADATA_KEY == "crosspoint_split"
114
+
115
+
116
+ def test_async_splits() -> None:
117
+ import asyncio
118
+
119
+ from crosspoint import CrosspointAsync
120
+
121
+ def answer(request: httpx.Request) -> httpx.Response:
122
+ return httpx.Response(204)
123
+
124
+ client = CrosspointAsync("cxp_oat_test")
125
+ client.splits._http._transport = httpx.MockTransport(answer)
126
+ assert asyncio.run(client.splits.delete("spl_1")) is None
@@ -44,7 +44,7 @@ wheels = [
44
44
 
45
45
  [[package]]
46
46
  name = "crosspoint"
47
- version = "0.2.0"
47
+ version = "0.3.0"
48
48
  source = { editable = "." }
49
49
  dependencies = [
50
50
  { name = "polar-sdk" },
@@ -1,9 +0,0 @@
1
- """Crosspoint API client: the Polar SDK, pointed at Crosspoint.
2
-
3
- The default API version is 2026-10; pin one with ``from crosspoint.v2026_10 import ...``.
4
- """
5
-
6
- from . import webhooks
7
- from .v2026_10 import CROSSPOINT_API_URL, Crosspoint, CrosspointAsync
8
-
9
- __all__ = ["CROSSPOINT_API_URL", "Crosspoint", "CrosspointAsync", "webhooks"]
@@ -1,16 +0,0 @@
1
- """Crosspoint's API is Polar's API: the Polar SDK works once it points at Crosspoint."""
2
-
3
- import os
4
-
5
- CROSSPOINT_API_URL = "https://api.crosspoint.finance"
6
- ACCESS_TOKEN_ENV = "CROSSPOINT_ACCESS_TOKEN"
7
-
8
-
9
- def resolve_access_token(access_token: str | None) -> str:
10
- token = access_token or os.environ.get(ACCESS_TOKEN_ENV)
11
- if not token:
12
- raise ValueError(
13
- f"Crosspoint: pass access_token or set {ACCESS_TOKEN_ENV} "
14
- "(an organization access token, cxp_oat_...)."
15
- )
16
- return token
@@ -1,40 +0,0 @@
1
- """Crosspoint client for API version 2026_04."""
2
-
3
- from typing import Any
4
-
5
- from polar.v2026_04 import Polar, PolarAsync
6
-
7
- from .._options import CROSSPOINT_API_URL, resolve_access_token
8
- from . import webhooks
9
-
10
- __all__ = ["CROSSPOINT_API_URL", "Crosspoint", "CrosspointAsync", "webhooks"]
11
-
12
-
13
- class Crosspoint(Polar):
14
- """`Crosspoint()` reads CROSSPOINT_ACCESS_TOKEN; pass `base_url` only for local development."""
15
-
16
- def __init__(
17
- self,
18
- access_token: str | None = None,
19
- *,
20
- base_url: str = CROSSPOINT_API_URL,
21
- timeout: Any = 5.0,
22
- ) -> None:
23
- super().__init__(
24
- resolve_access_token(access_token), base_url=base_url, timeout=timeout
25
- )
26
-
27
-
28
- class CrosspointAsync(PolarAsync):
29
- """Async client: `async with CrosspointAsync() as crosspoint: ...`."""
30
-
31
- def __init__(
32
- self,
33
- access_token: str | None = None,
34
- *,
35
- base_url: str = CROSSPOINT_API_URL,
36
- timeout: Any = 5.0,
37
- ) -> None:
38
- super().__init__(
39
- resolve_access_token(access_token), base_url=base_url, timeout=timeout
40
- )
@@ -1,40 +0,0 @@
1
- """Crosspoint client for API version 2026_10."""
2
-
3
- from typing import Any
4
-
5
- from polar.v2026_10 import Polar, PolarAsync
6
-
7
- from .._options import CROSSPOINT_API_URL, resolve_access_token
8
- from . import webhooks
9
-
10
- __all__ = ["CROSSPOINT_API_URL", "Crosspoint", "CrosspointAsync", "webhooks"]
11
-
12
-
13
- class Crosspoint(Polar):
14
- """`Crosspoint()` reads CROSSPOINT_ACCESS_TOKEN; pass `base_url` only for local development."""
15
-
16
- def __init__(
17
- self,
18
- access_token: str | None = None,
19
- *,
20
- base_url: str = CROSSPOINT_API_URL,
21
- timeout: Any = 5.0,
22
- ) -> None:
23
- super().__init__(
24
- resolve_access_token(access_token), base_url=base_url, timeout=timeout
25
- )
26
-
27
-
28
- class CrosspointAsync(PolarAsync):
29
- """Async client: `async with CrosspointAsync() as crosspoint: ...`."""
30
-
31
- def __init__(
32
- self,
33
- access_token: str | None = None,
34
- *,
35
- base_url: str = CROSSPOINT_API_URL,
36
- timeout: Any = 5.0,
37
- ) -> None:
38
- super().__init__(
39
- resolve_access_token(access_token), base_url=base_url, timeout=timeout
40
- )
@@ -1,40 +0,0 @@
1
- """Crosspoint client for API version 2027_01."""
2
-
3
- from typing import Any
4
-
5
- from polar.v2027_01 import Polar, PolarAsync
6
-
7
- from .._options import CROSSPOINT_API_URL, resolve_access_token
8
- from . import webhooks
9
-
10
- __all__ = ["CROSSPOINT_API_URL", "Crosspoint", "CrosspointAsync", "webhooks"]
11
-
12
-
13
- class Crosspoint(Polar):
14
- """`Crosspoint()` reads CROSSPOINT_ACCESS_TOKEN; pass `base_url` only for local development."""
15
-
16
- def __init__(
17
- self,
18
- access_token: str | None = None,
19
- *,
20
- base_url: str = CROSSPOINT_API_URL,
21
- timeout: Any = 5.0,
22
- ) -> None:
23
- super().__init__(
24
- resolve_access_token(access_token), base_url=base_url, timeout=timeout
25
- )
26
-
27
-
28
- class CrosspointAsync(PolarAsync):
29
- """Async client: `async with CrosspointAsync() as crosspoint: ...`."""
30
-
31
- def __init__(
32
- self,
33
- access_token: str | None = None,
34
- *,
35
- base_url: str = CROSSPOINT_API_URL,
36
- timeout: Any = 5.0,
37
- ) -> None:
38
- super().__init__(
39
- resolve_access_token(access_token), base_url=base_url, timeout=timeout
40
- )
@@ -1,66 +0,0 @@
1
- import httpx
2
- import pytest
3
-
4
- from crosspoint import CROSSPOINT_API_URL, Crosspoint
5
- from crosspoint.v2026_04 import Crosspoint as Crosspoint202604
6
-
7
-
8
- def http(client: Crosspoint) -> httpx.Client:
9
- return client._client._client # the SDK's httpx client
10
-
11
-
12
- def test_talks_to_crosspoint_not_polar() -> None:
13
- client = http(Crosspoint("cxp_oat_test"))
14
-
15
- assert str(client.base_url).rstrip("/") == CROSSPOINT_API_URL
16
- assert client.headers["authorization"] == "Bearer cxp_oat_test"
17
-
18
-
19
- def test_local_development_url() -> None:
20
- client = http(Crosspoint("cxp_oat_test", base_url="http://127.0.0.1:8001"))
21
- assert str(client.base_url).rstrip("/") == "http://127.0.0.1:8001"
22
-
23
-
24
- def test_reads_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
25
- monkeypatch.setenv("CROSSPOINT_ACCESS_TOKEN", "cxp_oat_env")
26
- Crosspoint()
27
- Crosspoint202604()
28
-
29
-
30
- def test_explains_a_missing_token(monkeypatch: pytest.MonkeyPatch) -> None:
31
- monkeypatch.delenv("CROSSPOINT_ACCESS_TOKEN", raising=False)
32
- with pytest.raises(ValueError, match="CROSSPOINT_ACCESS_TOKEN"):
33
- Crosspoint()
34
-
35
-
36
- def test_every_api_version(monkeypatch: pytest.MonkeyPatch) -> None:
37
- from crosspoint.v2027_01 import Crosspoint as Crosspoint202701
38
-
39
- client = http(Crosspoint202701("cxp_oat_test"))
40
- assert str(client.base_url).rstrip("/") == CROSSPOINT_API_URL
41
-
42
-
43
- def test_webhooks_import_paths() -> None:
44
- import crosspoint
45
- from crosspoint.v2027_01.webhooks import validate_event as pinned
46
- from crosspoint.webhooks import (
47
- PolarWebhookVerificationError,
48
- WebhookUnknownTypeError,
49
- WebhookVerificationError,
50
- validate_event,
51
- )
52
-
53
- assert crosspoint.webhooks.validate_event is validate_event
54
- assert callable(pinned)
55
- assert WebhookVerificationError is PolarWebhookVerificationError
56
- assert issubclass(WebhookUnknownTypeError, Exception)
57
- with pytest.raises(WebhookVerificationError):
58
- validate_event(
59
- "{}",
60
- {
61
- "webhook-id": "x",
62
- "webhook-timestamp": "1",
63
- "webhook-signature": "v1,bad",
64
- },
65
- "secret",
66
- )
File without changes