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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .DS_Store
@@ -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