startupaid 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.
- startupaid-0.1.0/.gitignore +8 -0
- startupaid-0.1.0/PKG-INFO +125 -0
- startupaid-0.1.0/README.md +110 -0
- startupaid-0.1.0/pyproject.toml +26 -0
- startupaid-0.1.0/src/startupaid/__init__.py +210 -0
- startupaid-0.1.0/src/startupaid/py.typed +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: startupaid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for startupAid — send email & push, schedule social posts, and convert currency.
|
|
5
|
+
Project-URL: Homepage, https://startupaid.org
|
|
6
|
+
Project-URL: Source, https://github.com/StartupAid-Org/startupaid-python
|
|
7
|
+
Author: startupAid
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: api,currency,email,push,sdk,social,startupaid
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# startupaid
|
|
17
|
+
|
|
18
|
+
Official Python SDK for [startupAid](https://startupaid.org) — send transactional
|
|
19
|
+
email, send push, schedule social posts, and convert currency, all with your
|
|
20
|
+
account API key. Pure standard library, no dependencies.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install startupaid
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires Python 3.9+.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from startupaid import Client
|
|
34
|
+
|
|
35
|
+
sa = Client("sk_your_key")
|
|
36
|
+
|
|
37
|
+
res = sa.convert("USD", "NGN", 100)
|
|
38
|
+
print(f"100 USD = {res['result']} NGN")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Create an API key in your startupAid dashboard → **API Keys**.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Email
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
msg = sa.send_email(
|
|
49
|
+
from_="Acme <hi@acme.com>",
|
|
50
|
+
to="user@example.com",
|
|
51
|
+
subject="Welcome",
|
|
52
|
+
body="<h1>Hi 👋</h1>",
|
|
53
|
+
)
|
|
54
|
+
status = sa.get_message(msg["id"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Currency
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
sa.convert("USD", "EUR", 250)
|
|
61
|
+
sa.rates("USD")
|
|
62
|
+
sa.currencies()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Push
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
sa.send_push(
|
|
69
|
+
"app_id",
|
|
70
|
+
target={"userRef": "u_123"},
|
|
71
|
+
title="Your order shipped",
|
|
72
|
+
body="Track it in the app.",
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Social
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
channels = sa.list_channels()
|
|
80
|
+
|
|
81
|
+
post = sa.schedule_post(
|
|
82
|
+
channels=[c["id"] for c in channels],
|
|
83
|
+
content="We just shipped 🚀",
|
|
84
|
+
# scheduled_for="2026-08-01T09:00:00Z" # omit to publish now
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
sa.list_posts()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Audiences
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
sa.list_audiences()
|
|
94
|
+
sa.add_contact("audience_id", email="new@example.com", first_name="Ada")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
Client(
|
|
101
|
+
"sk_your_key",
|
|
102
|
+
base_url="http://localhost:8080", # self-host / testing
|
|
103
|
+
timeout=30.0, # per-request timeout (seconds)
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Errors
|
|
108
|
+
|
|
109
|
+
Non-2xx responses raise `StartupAidError` with the status code and message:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from startupaid import StartupAidError
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
sa.convert("USD", "NGN", 1)
|
|
116
|
+
except StartupAidError as err:
|
|
117
|
+
print(err.status, err.message)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Each method requires your account to be subscribed to the matching product
|
|
121
|
+
(email, currency, push, social); otherwise the API returns an upgrade prompt.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# startupaid
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [startupAid](https://startupaid.org) — send transactional
|
|
4
|
+
email, send push, schedule social posts, and convert currency, all with your
|
|
5
|
+
account API key. Pure standard library, no dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install startupaid
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python 3.9+.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from startupaid import Client
|
|
19
|
+
|
|
20
|
+
sa = Client("sk_your_key")
|
|
21
|
+
|
|
22
|
+
res = sa.convert("USD", "NGN", 100)
|
|
23
|
+
print(f"100 USD = {res['result']} NGN")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Create an API key in your startupAid dashboard → **API Keys**.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Email
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
msg = sa.send_email(
|
|
34
|
+
from_="Acme <hi@acme.com>",
|
|
35
|
+
to="user@example.com",
|
|
36
|
+
subject="Welcome",
|
|
37
|
+
body="<h1>Hi 👋</h1>",
|
|
38
|
+
)
|
|
39
|
+
status = sa.get_message(msg["id"])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Currency
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
sa.convert("USD", "EUR", 250)
|
|
46
|
+
sa.rates("USD")
|
|
47
|
+
sa.currencies()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Push
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
sa.send_push(
|
|
54
|
+
"app_id",
|
|
55
|
+
target={"userRef": "u_123"},
|
|
56
|
+
title="Your order shipped",
|
|
57
|
+
body="Track it in the app.",
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Social
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
channels = sa.list_channels()
|
|
65
|
+
|
|
66
|
+
post = sa.schedule_post(
|
|
67
|
+
channels=[c["id"] for c in channels],
|
|
68
|
+
content="We just shipped 🚀",
|
|
69
|
+
# scheduled_for="2026-08-01T09:00:00Z" # omit to publish now
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
sa.list_posts()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Audiences
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
sa.list_audiences()
|
|
79
|
+
sa.add_contact("audience_id", email="new@example.com", first_name="Ada")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
Client(
|
|
86
|
+
"sk_your_key",
|
|
87
|
+
base_url="http://localhost:8080", # self-host / testing
|
|
88
|
+
timeout=30.0, # per-request timeout (seconds)
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Errors
|
|
93
|
+
|
|
94
|
+
Non-2xx responses raise `StartupAidError` with the status code and message:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from startupaid import StartupAidError
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
sa.convert("USD", "NGN", 1)
|
|
101
|
+
except StartupAidError as err:
|
|
102
|
+
print(err.status, err.message)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Each method requires your account to be subscribed to the matching product
|
|
106
|
+
(email, currency, push, social); otherwise the API returns an upgrade prompt.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "startupaid"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for startupAid — send email & push, schedule social posts, and convert currency."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "startupAid" }]
|
|
13
|
+
keywords = ["startupaid", "email", "push", "social", "currency", "api", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://startupaid.org"
|
|
23
|
+
Source = "https://github.com/StartupAid-Org/startupaid-python"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/startupaid"]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Official Python SDK for startupAid.
|
|
2
|
+
|
|
3
|
+
Send transactional email, send push, schedule social posts, and convert currency
|
|
4
|
+
with your account API key.
|
|
5
|
+
|
|
6
|
+
from startupaid import Client
|
|
7
|
+
|
|
8
|
+
sa = Client("sk_your_key")
|
|
9
|
+
res = sa.convert("USD", "NGN", 100)
|
|
10
|
+
print(res["result"])
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.parse
|
|
18
|
+
import urllib.request
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
__all__ = ["Client", "StartupAidError", "DEFAULT_BASE_URL", "__version__"]
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "https://api.startupaid.org"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StartupAidError(Exception):
|
|
28
|
+
"""Raised when the API responds with a non-2xx status."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, status: int, message: str) -> None:
|
|
31
|
+
super().__init__(f"startupaid: {status}: {message}")
|
|
32
|
+
self.status = status
|
|
33
|
+
self.message = message
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Client:
|
|
37
|
+
"""A startupAid API client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
api_key: Your account API key.
|
|
41
|
+
base_url: Override the API base URL (self-hosting / testing).
|
|
42
|
+
timeout: Per-request timeout in seconds.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
api_key: str,
|
|
48
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
49
|
+
timeout: float = 30.0,
|
|
50
|
+
) -> None:
|
|
51
|
+
if not api_key:
|
|
52
|
+
raise ValueError("api_key is required")
|
|
53
|
+
self.api_key = api_key
|
|
54
|
+
self.base_url = base_url.rstrip("/")
|
|
55
|
+
self.timeout = timeout
|
|
56
|
+
|
|
57
|
+
# ---- transport ----
|
|
58
|
+
|
|
59
|
+
def _request(
|
|
60
|
+
self,
|
|
61
|
+
method: str,
|
|
62
|
+
path: str,
|
|
63
|
+
query: dict[str, Any] | None = None,
|
|
64
|
+
body: Any | None = None,
|
|
65
|
+
) -> Any:
|
|
66
|
+
url = self.base_url + path
|
|
67
|
+
if query:
|
|
68
|
+
clean = {k: v for k, v in query.items() if v is not None}
|
|
69
|
+
if clean:
|
|
70
|
+
url += "?" + urllib.parse.urlencode(clean)
|
|
71
|
+
|
|
72
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
73
|
+
req = urllib.request.Request(url, data=data, method=method)
|
|
74
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
75
|
+
req.add_header("Accept", "application/json")
|
|
76
|
+
if data is not None:
|
|
77
|
+
req.add_header("Content-Type", "application/json")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
81
|
+
text = resp.read().decode()
|
|
82
|
+
return json.loads(text) if text else {}
|
|
83
|
+
except urllib.error.HTTPError as exc:
|
|
84
|
+
text = exc.read().decode()
|
|
85
|
+
message = text.strip()
|
|
86
|
+
try:
|
|
87
|
+
parsed = json.loads(text)
|
|
88
|
+
if isinstance(parsed, dict) and parsed.get("error"):
|
|
89
|
+
message = str(parsed["error"])
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
92
|
+
raise StartupAidError(exc.code, message) from None
|
|
93
|
+
|
|
94
|
+
# ---- Email ----
|
|
95
|
+
|
|
96
|
+
def send_email(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
from_: str,
|
|
100
|
+
to: str,
|
|
101
|
+
subject: str | None = None,
|
|
102
|
+
body: str | None = None,
|
|
103
|
+
template: str | None = None,
|
|
104
|
+
variables: dict[str, str] | None = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""Send a transactional email. Provide ``body`` (HTML) or a ``template``."""
|
|
107
|
+
payload: dict[str, Any] = {"from": from_, "to": to}
|
|
108
|
+
if subject is not None:
|
|
109
|
+
payload["subject"] = subject
|
|
110
|
+
if body is not None:
|
|
111
|
+
payload["body"] = body
|
|
112
|
+
if template is not None:
|
|
113
|
+
payload["template"] = template
|
|
114
|
+
if variables is not None:
|
|
115
|
+
payload["variables"] = variables
|
|
116
|
+
return self._request("POST", "/v1/send", body=payload)
|
|
117
|
+
|
|
118
|
+
def get_message(self, message_id: str) -> dict[str, Any]:
|
|
119
|
+
"""Look up a sent email's status by id."""
|
|
120
|
+
return self._request("GET", f"/v1/messages/{urllib.parse.quote(message_id)}")
|
|
121
|
+
|
|
122
|
+
# ---- Currency ----
|
|
123
|
+
|
|
124
|
+
def convert(self, from_: str, to: str, amount: float = 1) -> dict[str, Any]:
|
|
125
|
+
"""Convert an amount between two currencies at live rates."""
|
|
126
|
+
return self._request("GET", "/v1/convert", query={"from": from_, "to": to, "amount": amount})
|
|
127
|
+
|
|
128
|
+
def rates(self, base: str) -> dict[str, Any]:
|
|
129
|
+
"""Exchange rates for a base currency against all supported currencies."""
|
|
130
|
+
return self._request("GET", f"/v1/rates/{urllib.parse.quote(base)}")
|
|
131
|
+
|
|
132
|
+
def currencies(self) -> Any:
|
|
133
|
+
"""List supported currencies."""
|
|
134
|
+
return self._request("GET", "/v1/currencies")
|
|
135
|
+
|
|
136
|
+
# ---- Push ----
|
|
137
|
+
|
|
138
|
+
def send_push(
|
|
139
|
+
self,
|
|
140
|
+
app_id: str,
|
|
141
|
+
*,
|
|
142
|
+
target: dict[str, Any],
|
|
143
|
+
title: str | None = None,
|
|
144
|
+
body: str | None = None,
|
|
145
|
+
link: str | None = None,
|
|
146
|
+
data: dict[str, str] | None = None,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Send a push notification to devices in a push app.
|
|
149
|
+
|
|
150
|
+
``target`` selects recipients, e.g. ``{"userRef": "u_1"}``,
|
|
151
|
+
``{"tokens": [...]}`` or ``{"all": True}``.
|
|
152
|
+
"""
|
|
153
|
+
payload: dict[str, Any] = {"target": target}
|
|
154
|
+
if title is not None:
|
|
155
|
+
payload["title"] = title
|
|
156
|
+
if body is not None:
|
|
157
|
+
payload["body"] = body
|
|
158
|
+
if link is not None:
|
|
159
|
+
payload["link"] = link
|
|
160
|
+
if data is not None:
|
|
161
|
+
payload["data"] = data
|
|
162
|
+
return self._request("POST", f"/v1/push/apps/{urllib.parse.quote(app_id)}/send", body=payload)
|
|
163
|
+
|
|
164
|
+
# ---- Social ----
|
|
165
|
+
|
|
166
|
+
def list_channels(self) -> list[dict[str, Any]]:
|
|
167
|
+
"""List connected social channels (use their ids with ``schedule_post``)."""
|
|
168
|
+
return self._request("GET", "/v1/social/channels").get("channels", [])
|
|
169
|
+
|
|
170
|
+
def schedule_post(
|
|
171
|
+
self,
|
|
172
|
+
*,
|
|
173
|
+
channels: list[str],
|
|
174
|
+
content: str,
|
|
175
|
+
image_url: str | None = None,
|
|
176
|
+
scheduled_for: str | None = None,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""Schedule or immediately publish a post. Omit ``scheduled_for`` to publish now."""
|
|
179
|
+
payload: dict[str, Any] = {"channels": channels, "content": content}
|
|
180
|
+
if image_url is not None:
|
|
181
|
+
payload["imageUrl"] = image_url
|
|
182
|
+
if scheduled_for is not None:
|
|
183
|
+
payload["scheduledFor"] = scheduled_for
|
|
184
|
+
return self._request("POST", "/v1/social/schedule", body=payload)
|
|
185
|
+
|
|
186
|
+
def list_posts(self) -> list[dict[str, Any]]:
|
|
187
|
+
"""List social posts with delivery status."""
|
|
188
|
+
return self._request("GET", "/v1/social/posts").get("posts", [])
|
|
189
|
+
|
|
190
|
+
# ---- Audiences ----
|
|
191
|
+
|
|
192
|
+
def list_audiences(self) -> list[dict[str, Any]]:
|
|
193
|
+
"""List mailing lists."""
|
|
194
|
+
return self._request("GET", "/v1/audiences").get("audiences", [])
|
|
195
|
+
|
|
196
|
+
def add_contact(
|
|
197
|
+
self,
|
|
198
|
+
audience_id: str,
|
|
199
|
+
*,
|
|
200
|
+
email: str,
|
|
201
|
+
first_name: str | None = None,
|
|
202
|
+
last_name: str | None = None,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""Add a contact to a mailing list."""
|
|
205
|
+
payload: dict[str, Any] = {"email": email}
|
|
206
|
+
if first_name is not None:
|
|
207
|
+
payload["firstName"] = first_name
|
|
208
|
+
if last_name is not None:
|
|
209
|
+
payload["lastName"] = last_name
|
|
210
|
+
return self._request("POST", f"/v1/audiences/{urllib.parse.quote(audience_id)}/contacts", body=payload)
|
|
File without changes
|