small-software 0.6.0__tar.gz → 0.7.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.
- {small_software-0.6.0 → small_software-0.7.0}/PKG-INFO +78 -13
- {small_software-0.6.0 → small_software-0.7.0}/README.md +77 -12
- {small_software-0.6.0 → small_software-0.7.0}/pyproject.toml +1 -1
- small_software-0.7.0/small_cli/__init__.py +1 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/access.py +30 -17
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/archive.py +6 -6
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/cli.py +10 -9
- small_software-0.7.0/small_cli/commands/__init__.py +8 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/access.py +119 -65
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/dashboard.py +1 -1
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/db.py +8 -8
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/deploy.py +15 -15
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/destroy.py +2 -2
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/domain.py +1 -1
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/env.py +8 -8
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/init.py +5 -5
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/link.py +1 -1
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/logs.py +1 -1
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/mail.py +8 -8
- small_software-0.7.0/small_cli/commands/mcp.py +23 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/notify.py +5 -5
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/commands/sleep.py +2 -2
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/common.py +4 -4
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/config.py +6 -6
- small_software-0.7.0/small_cli/dashboard.py +1108 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/i18n.py +10 -9
- small_software-0.7.0/small_cli/mcp.py +364 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/railway.py +25 -25
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/scaffold.py +11 -11
- {small_software-0.6.0/small_cli/templates/python-http → small_software-0.7.0/small_cli/templates/fastapi}/magic_link.py +223 -90
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/templates/node-http/magic-link.js +49 -45
- {small_software-0.6.0/small_cli/templates/fastapi → small_software-0.7.0/small_cli/templates/python-http}/magic_link.py +223 -90
- small_software-0.7.0/small_cli/templates/streamlit/app.py +249 -0
- small_software-0.7.0/small_cli/templates/streamlit/magic_link.py +808 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_software.egg-info/PKG-INFO +78 -13
- {small_software-0.6.0 → small_software-0.7.0}/small_software.egg-info/SOURCES.txt +7 -1
- small_software-0.7.0/tests/test_dashboard_share.py +355 -0
- small_software-0.7.0/tests/test_mcp.py +229 -0
- {small_software-0.6.0 → small_software-0.7.0}/tests/test_small.py +124 -121
- small_software-0.7.0/tests/test_streamlit_email.py +434 -0
- small_software-0.6.0/small_cli/__init__.py +0 -1
- small_software-0.6.0/small_cli/commands/__init__.py +0 -8
- small_software-0.6.0/small_cli/dashboard.py +0 -648
- small_software-0.6.0/small_cli/templates/streamlit/app.py +0 -181
- {small_software-0.6.0 → small_software-0.7.0}/LICENSE +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/setup.cfg +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/__main__.py +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/templates/fastapi/main.py +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/templates/fastapi/requirements.txt +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/templates/node-http/package.json +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/templates/node-http/server.js +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/templates/python-http/app.py +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_cli/templates/streamlit/requirements.txt +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_software.egg-info/dependency_links.txt +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_software.egg-info/entry_points.txt +0 -0
- {small_software-0.6.0 → small_software-0.7.0}/small_software.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: small-software
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: A cloud for small software: deploy team apps to Railway in one command, private by default, shared by work email
|
|
5
5
|
Author: RuslanKazyradzi13
|
|
6
6
|
License-Expression: MIT
|
|
@@ -70,8 +70,25 @@ From source, in a clone of the repository:
|
|
|
70
70
|
pip install ./small-cli
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
**Windows
|
|
74
|
-
|
|
73
|
+
**Windows (recommended): one command in PowerShell.**
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
irm https://raw.githubusercontent.com/RuslanKazyradzi13/small-software/main/install.ps1 | iex
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It installs small into its own Python environment in `%LOCALAPPDATA%\Programs\small` and adds a `small`
|
|
80
|
+
command that Smart App Control does not block (there is no unsigned `small.exe`). Needs Python 3.9+, no admin
|
|
81
|
+
rights; run it again to upgrade. Uninstall:
|
|
82
|
+
|
|
83
|
+
```powershell
|
|
84
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/RuslanKazyradzi13/small-software/main/install.ps1))) -Uninstall
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Values with `&` or `|` (URLs, for example) need PowerShell's stop-parsing token:
|
|
88
|
+
`small --% env set "URL=https://x?a=1&b=2"`.
|
|
89
|
+
|
|
90
|
+
**Windows with pip.** `pip install` creates `small.exe`, which Smart App Control / Application Control may
|
|
91
|
+
block because it is unsigned. If that happens, either:
|
|
75
92
|
|
|
76
93
|
- run small through Python, which works everywhere: `python -m small_cli deploy` (any command works this way);
|
|
77
94
|
- or add the `small-cli` folder of a clone to `PATH`, and `small.cmd` will serve the `small` command. Run this in
|
|
@@ -154,12 +171,15 @@ The sample apps are in English: both the page text and the code comments.
|
|
|
154
171
|
`small deploy` passes `ACCESS_TOKEN` from `small.json` to Railway. The app opens at
|
|
155
172
|
`https://<domain>/?token=<ACCESS_TOKEN>`; without the token visitors get a 403 page with a **Request access**
|
|
156
173
|
button. The server listens on `$HOST:$PORT`, by default `0.0.0.0` and the `PORT` set by Railway.
|
|
157
|
-
- **The `streamlit` template.** Streamlit
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
- **The `streamlit` template.** Streamlit can't be wrapped in middleware, so `require_access()` in `app.py` uses
|
|
175
|
+
`MagicLinkGate` from `magic_link.py`: the same variables (`ACCESS_EMAILS`, `ACCESS_TOKEN`, `SESSION_SECRET`,
|
|
176
|
+
`ACCESS_REQUEST_URL`...), one-time links, limits and logs as the other templates, and the access screen follows the
|
|
177
|
+
visitor's browser language. Differences:
|
|
178
|
+
- Sign-in by email: the link leads to `https://<domain>/?t=...` and shows a **Sign in** button; only the click
|
|
179
|
+
signs in. The 30-day session is a signed cookie written by a tiny same-origin frame, so unlike in the other
|
|
180
|
+
templates it is **not HttpOnly**: don't render untrusted HTML in a protected app. **Sign out** is in the sidebar.
|
|
181
|
+
- Personal links keep the token in the address bar (`?token=`), which is how they survive a page reload.
|
|
182
|
+
- Streamlit is a single-page app, so the "no access" screen is served with HTTP 200, not 403.
|
|
163
183
|
Railway runs `python app.py`, and `app.py` restarts itself through `streamlit run` on `$HOST:$PORT`.
|
|
164
184
|
Run it locally the same way: `python app.py`.
|
|
165
185
|
- **Name.** By default, the folder name plus a random suffix, for example `my-service-3f9a`. An explicit name in
|
|
@@ -204,6 +224,12 @@ Buttons on the card of a deployed app:
|
|
|
204
224
|
The token only goes to the browser's address bar on navigation; it is not in the dashboard page data.
|
|
205
225
|
- **Logs** opens a window with the last 200 lines of the app or build log (tabs **App** / **Build**).
|
|
206
226
|
Requires `RAILWAY_API_TOKEN`; without it the button is disabled.
|
|
227
|
+
- **Share** opens who has access: work emails, whole domains and personal-link names (never the links
|
|
228
|
+
themselves). Type `ivan@company.com`, `@company.com` or a name and click **Add**, or click **Revoke** next to
|
|
229
|
+
anyone. It does exactly what `small access add` / `revoke` does: updates small.json and Railway and restarts
|
|
230
|
+
the app. A new personal link is shown only once, with a Copy button, and works like a password. Requires
|
|
231
|
+
`RAILWAY_API_TOKEN`. The page and its requests are protected: a per-run key, `Host`/`Origin` checks, no
|
|
232
|
+
framing, JSON only.
|
|
207
233
|
|
|
208
234
|
- **Port** 3000 by default; if it is busy, any free port is used. The URL is printed to stdout.
|
|
209
235
|
- **The server listens on `127.0.0.1` only** and answers 403 to requests with a foreign `Host` (DNS rebinding
|
|
@@ -247,6 +273,46 @@ $10 per GB of memory, $0.15 per GB of volume. The result reads "≈ $X/mo at cur
|
|
|
247
273
|
- **This is an estimate, not a bill.** Egress ($0.05/GB), the plan's free allowance ($5 on Hobby, $20 on Pro) and
|
|
248
274
|
the subscription are not included. The exact amount is in Railway → Usage.
|
|
249
275
|
|
|
276
|
+
## `small mcp`: AI agents (Claude Code, Cursor...)
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
small mcp
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Runs an [MCP](https://modelcontextprotocol.io) server on stdin/stdout, so an AI coding agent can create, deploy and
|
|
283
|
+
share apps itself: *"Deploy this folder and give ivan@company.com access."*
|
|
284
|
+
|
|
285
|
+
**Connect it:**
|
|
286
|
+
- **Claude Code:** `claude mcp add small -- small mcp` (Windows: `claude mcp add small -- cmd /c small mcp`).
|
|
287
|
+
- **Cursor** (`~/.cursor/mcp.json`), **Claude Desktop**, **Windsurf** and other clients:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{"mcpServers": {"small": {"command": "small", "args": ["mcp"]}}}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
On Windows use `"command": "cmd", "args": ["/c", "small", "mcp"]`, or `"command": "python", "args": ["-m",
|
|
294
|
+
"small_cli", "mcp"]` if small was installed with pip.
|
|
295
|
+
- `RAILWAY_API_TOKEN` must be in the environment the agent starts from. After setting it, restart the agent.
|
|
296
|
+
|
|
297
|
+
**Tools:**
|
|
298
|
+
|
|
299
|
+
| Tool | What it does |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `init_app` | `small init`: a ready app from a template |
|
|
302
|
+
| `deploy_app` | `small deploy`: returns the URL; progress is streamed while it builds |
|
|
303
|
+
| `share_app` | `small access add`: an email, `@domain` or a personal-link name |
|
|
304
|
+
| `revoke_access` | `small access revoke` |
|
|
305
|
+
| `list_access` | `small access list` |
|
|
306
|
+
| `list_apps` | apps under a folder with URL and latest deploy status |
|
|
307
|
+
| `app_logs` | `small logs`: app or build log |
|
|
308
|
+
| `set_env` | `small env set` / `unset` |
|
|
309
|
+
|
|
310
|
+
- **No delete tool:** `small destroy` stays a terminal command.
|
|
311
|
+
- **Prefer emails.** A personal link (a name instead of an email) is returned to the agent, so it passes through the
|
|
312
|
+
chat like any other text; with email sign-in nothing secret does.
|
|
313
|
+
- The tools run the same code as the commands and answer in small's language (`SMALL_LANG`). Your MCP client
|
|
314
|
+
normally asks you before each tool call.
|
|
315
|
+
|
|
250
316
|
## `small deploy`
|
|
251
317
|
|
|
252
318
|
```
|
|
@@ -315,8 +381,7 @@ then takes effect on the next deploy.
|
|
|
315
381
|
- Someone who is not on the list and enters their email gets the same "Check your email" response, and the owner
|
|
316
382
|
receives an access request (in the log, and in Telegram if connected) with a ready-made
|
|
317
383
|
`small access add their@address` command.
|
|
318
|
-
- Works in
|
|
319
|
-
now.
|
|
384
|
+
- Works in all templates: `python-http`, `fastapi`, `node-http` and `streamlit` (see the `streamlit` notes above).
|
|
320
385
|
|
|
321
386
|
*By personal invite link*:
|
|
322
387
|
- **`add bob`** creates a token, adds it to `ACCESS_TOKEN` as `bob:token`, restarts the app and prints the link.
|
|
@@ -501,8 +566,8 @@ The tests start a local mock of the Railway API (GraphQL + the upload endpoint)
|
|
|
501
566
|
deploy, repeat deploy, failed build, crash, taken domain, several workspaces, wrong token, ignore rules.
|
|
502
567
|
|
|
503
568
|
Template tests on real frameworks are skipped if the framework is not installed; the rest pass.
|
|
504
|
-
-
|
|
505
|
-
through `python app.py`.
|
|
569
|
+
- `streamlit` tests: the access gate through `streamlit.testing.v1.AppTest` (personal tokens and sign-in by email:
|
|
570
|
+
form, one-time link, session cookie, revocation, sign out) and the server through `python app.py`.
|
|
506
571
|
- 2 `fastapi` tests: `TestClient` and `uvicorn main:app` the way Railway runs it, plus a check that the token is
|
|
507
572
|
hidden in the access log.
|
|
508
573
|
|
|
@@ -42,8 +42,25 @@ From source, in a clone of the repository:
|
|
|
42
42
|
pip install ./small-cli
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
**Windows
|
|
46
|
-
|
|
45
|
+
**Windows (recommended): one command in PowerShell.**
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
irm https://raw.githubusercontent.com/RuslanKazyradzi13/small-software/main/install.ps1 | iex
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It installs small into its own Python environment in `%LOCALAPPDATA%\Programs\small` and adds a `small`
|
|
52
|
+
command that Smart App Control does not block (there is no unsigned `small.exe`). Needs Python 3.9+, no admin
|
|
53
|
+
rights; run it again to upgrade. Uninstall:
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/RuslanKazyradzi13/small-software/main/install.ps1))) -Uninstall
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Values with `&` or `|` (URLs, for example) need PowerShell's stop-parsing token:
|
|
60
|
+
`small --% env set "URL=https://x?a=1&b=2"`.
|
|
61
|
+
|
|
62
|
+
**Windows with pip.** `pip install` creates `small.exe`, which Smart App Control / Application Control may
|
|
63
|
+
block because it is unsigned. If that happens, either:
|
|
47
64
|
|
|
48
65
|
- run small through Python, which works everywhere: `python -m small_cli deploy` (any command works this way);
|
|
49
66
|
- or add the `small-cli` folder of a clone to `PATH`, and `small.cmd` will serve the `small` command. Run this in
|
|
@@ -126,12 +143,15 @@ The sample apps are in English: both the page text and the code comments.
|
|
|
126
143
|
`small deploy` passes `ACCESS_TOKEN` from `small.json` to Railway. The app opens at
|
|
127
144
|
`https://<domain>/?token=<ACCESS_TOKEN>`; without the token visitors get a 403 page with a **Request access**
|
|
128
145
|
button. The server listens on `$HOST:$PORT`, by default `0.0.0.0` and the `PORT` set by Railway.
|
|
129
|
-
- **The `streamlit` template.** Streamlit
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
- **The `streamlit` template.** Streamlit can't be wrapped in middleware, so `require_access()` in `app.py` uses
|
|
147
|
+
`MagicLinkGate` from `magic_link.py`: the same variables (`ACCESS_EMAILS`, `ACCESS_TOKEN`, `SESSION_SECRET`,
|
|
148
|
+
`ACCESS_REQUEST_URL`...), one-time links, limits and logs as the other templates, and the access screen follows the
|
|
149
|
+
visitor's browser language. Differences:
|
|
150
|
+
- Sign-in by email: the link leads to `https://<domain>/?t=...` and shows a **Sign in** button; only the click
|
|
151
|
+
signs in. The 30-day session is a signed cookie written by a tiny same-origin frame, so unlike in the other
|
|
152
|
+
templates it is **not HttpOnly**: don't render untrusted HTML in a protected app. **Sign out** is in the sidebar.
|
|
153
|
+
- Personal links keep the token in the address bar (`?token=`), which is how they survive a page reload.
|
|
154
|
+
- Streamlit is a single-page app, so the "no access" screen is served with HTTP 200, not 403.
|
|
135
155
|
Railway runs `python app.py`, and `app.py` restarts itself through `streamlit run` on `$HOST:$PORT`.
|
|
136
156
|
Run it locally the same way: `python app.py`.
|
|
137
157
|
- **Name.** By default, the folder name plus a random suffix, for example `my-service-3f9a`. An explicit name in
|
|
@@ -176,6 +196,12 @@ Buttons on the card of a deployed app:
|
|
|
176
196
|
The token only goes to the browser's address bar on navigation; it is not in the dashboard page data.
|
|
177
197
|
- **Logs** opens a window with the last 200 lines of the app or build log (tabs **App** / **Build**).
|
|
178
198
|
Requires `RAILWAY_API_TOKEN`; without it the button is disabled.
|
|
199
|
+
- **Share** opens who has access: work emails, whole domains and personal-link names (never the links
|
|
200
|
+
themselves). Type `ivan@company.com`, `@company.com` or a name and click **Add**, or click **Revoke** next to
|
|
201
|
+
anyone. It does exactly what `small access add` / `revoke` does: updates small.json and Railway and restarts
|
|
202
|
+
the app. A new personal link is shown only once, with a Copy button, and works like a password. Requires
|
|
203
|
+
`RAILWAY_API_TOKEN`. The page and its requests are protected: a per-run key, `Host`/`Origin` checks, no
|
|
204
|
+
framing, JSON only.
|
|
179
205
|
|
|
180
206
|
- **Port** 3000 by default; if it is busy, any free port is used. The URL is printed to stdout.
|
|
181
207
|
- **The server listens on `127.0.0.1` only** and answers 403 to requests with a foreign `Host` (DNS rebinding
|
|
@@ -219,6 +245,46 @@ $10 per GB of memory, $0.15 per GB of volume. The result reads "≈ $X/mo at cur
|
|
|
219
245
|
- **This is an estimate, not a bill.** Egress ($0.05/GB), the plan's free allowance ($5 on Hobby, $20 on Pro) and
|
|
220
246
|
the subscription are not included. The exact amount is in Railway → Usage.
|
|
221
247
|
|
|
248
|
+
## `small mcp`: AI agents (Claude Code, Cursor...)
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
small mcp
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Runs an [MCP](https://modelcontextprotocol.io) server on stdin/stdout, so an AI coding agent can create, deploy and
|
|
255
|
+
share apps itself: *"Deploy this folder and give ivan@company.com access."*
|
|
256
|
+
|
|
257
|
+
**Connect it:**
|
|
258
|
+
- **Claude Code:** `claude mcp add small -- small mcp` (Windows: `claude mcp add small -- cmd /c small mcp`).
|
|
259
|
+
- **Cursor** (`~/.cursor/mcp.json`), **Claude Desktop**, **Windsurf** and other clients:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{"mcpServers": {"small": {"command": "small", "args": ["mcp"]}}}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
On Windows use `"command": "cmd", "args": ["/c", "small", "mcp"]`, or `"command": "python", "args": ["-m",
|
|
266
|
+
"small_cli", "mcp"]` if small was installed with pip.
|
|
267
|
+
- `RAILWAY_API_TOKEN` must be in the environment the agent starts from. After setting it, restart the agent.
|
|
268
|
+
|
|
269
|
+
**Tools:**
|
|
270
|
+
|
|
271
|
+
| Tool | What it does |
|
|
272
|
+
|---|---|
|
|
273
|
+
| `init_app` | `small init`: a ready app from a template |
|
|
274
|
+
| `deploy_app` | `small deploy`: returns the URL; progress is streamed while it builds |
|
|
275
|
+
| `share_app` | `small access add`: an email, `@domain` or a personal-link name |
|
|
276
|
+
| `revoke_access` | `small access revoke` |
|
|
277
|
+
| `list_access` | `small access list` |
|
|
278
|
+
| `list_apps` | apps under a folder with URL and latest deploy status |
|
|
279
|
+
| `app_logs` | `small logs`: app or build log |
|
|
280
|
+
| `set_env` | `small env set` / `unset` |
|
|
281
|
+
|
|
282
|
+
- **No delete tool:** `small destroy` stays a terminal command.
|
|
283
|
+
- **Prefer emails.** A personal link (a name instead of an email) is returned to the agent, so it passes through the
|
|
284
|
+
chat like any other text; with email sign-in nothing secret does.
|
|
285
|
+
- The tools run the same code as the commands and answer in small's language (`SMALL_LANG`). Your MCP client
|
|
286
|
+
normally asks you before each tool call.
|
|
287
|
+
|
|
222
288
|
## `small deploy`
|
|
223
289
|
|
|
224
290
|
```
|
|
@@ -287,8 +353,7 @@ then takes effect on the next deploy.
|
|
|
287
353
|
- Someone who is not on the list and enters their email gets the same "Check your email" response, and the owner
|
|
288
354
|
receives an access request (in the log, and in Telegram if connected) with a ready-made
|
|
289
355
|
`small access add their@address` command.
|
|
290
|
-
- Works in
|
|
291
|
-
now.
|
|
356
|
+
- Works in all templates: `python-http`, `fastapi`, `node-http` and `streamlit` (see the `streamlit` notes above).
|
|
292
357
|
|
|
293
358
|
*By personal invite link*:
|
|
294
359
|
- **`add bob`** creates a token, adds it to `ACCESS_TOKEN` as `bob:token`, restarts the app and prints the link.
|
|
@@ -473,8 +538,8 @@ The tests start a local mock of the Railway API (GraphQL + the upload endpoint)
|
|
|
473
538
|
deploy, repeat deploy, failed build, crash, taken domain, several workspaces, wrong token, ignore rules.
|
|
474
539
|
|
|
475
540
|
Template tests on real frameworks are skipped if the framework is not installed; the rest pass.
|
|
476
|
-
-
|
|
477
|
-
through `python app.py`.
|
|
541
|
+
- `streamlit` tests: the access gate through `streamlit.testing.v1.AppTest` (personal tokens and sign-in by email:
|
|
542
|
+
form, one-time link, session cookie, revocation, sign out) and the server through `python app.py`.
|
|
478
543
|
- 2 `fastapi` tests: `TestClient` and `uvicorn main:app` the way Railway runs it, plus a check that the token is
|
|
479
544
|
hidden in the access log.
|
|
480
545
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "small-software"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "A cloud for small software: deploy team apps to Railway in one command, private by default, shared by work email"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENSE"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""App access: ACCESS_TOKEN (invite links "name:token,…") and ACCESS_EMAILS (sign-in by email).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
magic_link.py, magic-link.js and the streamlit template understand the same format: remove someone's token and
|
|
4
|
+
their link and cookie stop working, while everyone else keeps access. Remove an address from ACCESS_EMAILS and
|
|
5
|
+
sign-in with it is closed at once.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
import re
|
|
@@ -38,12 +39,12 @@ def new_token():
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def is_email(value):
|
|
41
|
-
"""
|
|
42
|
+
"""An email address or a whole domain "@company.com"."""
|
|
42
43
|
return bool(EMAIL_RE.match(value) or DOMAIN_RE.match(value))
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def parse_emails(value):
|
|
46
|
-
""""Ivan@Company.com, @team.com" -> ["ivan@company.com", "@team.com"]:
|
|
47
|
+
""""Ivan@Company.com, @team.com" -> ["ivan@company.com", "@team.com"]: lowercased, without duplicates or junk."""
|
|
47
48
|
found = []
|
|
48
49
|
for item in (value or "").split(","):
|
|
49
50
|
item = item.strip().lower()
|
|
@@ -53,7 +54,7 @@ def parse_emails(value):
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def app_env(root, state, warn=None):
|
|
56
|
-
"""(
|
|
57
|
+
"""(app variables, where each one comes from): Railway, overlaid with non-empty values from small.json."""
|
|
57
58
|
env, sources = {}, {}
|
|
58
59
|
linked = all(state.get(k) for k in ("projectId", "environmentId", "serviceId"))
|
|
59
60
|
if linked and token_from_env():
|
|
@@ -72,7 +73,7 @@ def app_env(root, state, warn=None):
|
|
|
72
73
|
|
|
73
74
|
|
|
74
75
|
def current(root, state, warn=None):
|
|
75
|
-
"""(
|
|
76
|
+
"""(ACCESS_TOKEN entries, source): from small.json, else from the service variables in Railway, else ([], None)."""
|
|
76
77
|
raw = load_config(root / CONFIG_FILE)["env"].get("ACCESS_TOKEN")
|
|
77
78
|
if raw:
|
|
78
79
|
return parse(raw), CONFIG_FILE
|
|
@@ -83,7 +84,7 @@ def current(root, state, warn=None):
|
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
def pick(entries, name=None):
|
|
86
|
-
"""
|
|
87
|
+
"""The token of the person `name`; without a name, the first one (the owner's). None if there is no such entry."""
|
|
87
88
|
if name is None:
|
|
88
89
|
return entries[0][1] if entries else None
|
|
89
90
|
return next((token for who, token in entries if who == name), None)
|
|
@@ -95,20 +96,32 @@ def invite_link(domain, token):
|
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
def email_login_problem(root):
|
|
98
|
-
"""None
|
|
99
|
-
|
|
99
|
+
"""None if the app code supports sign-in by email; otherwise, what is in the way and how to fix it.
|
|
100
|
+
|
|
101
|
+
Streamlit cannot be wrapped in middleware: there sign-in by email is done by require_access() in app.py via
|
|
102
|
+
MagicLinkGate from magic_link.py, so new versions of both files are needed."""
|
|
103
|
+
app = root / "app.py"
|
|
104
|
+
source = app.read_text(encoding="utf-8", errors="replace") if app.is_file() else ""
|
|
105
|
+
streamlit = "streamlit" in source
|
|
106
|
+
if streamlit and "MagicLinkGate" not in source:
|
|
107
|
+
return (tr("app.py is from an old streamlit template without sign-in by email. Take require_access() from "
|
|
108
|
+
"the new app.py, and magic_link.py next to it: %s (or grant a personal link: small access add NAME)",
|
|
109
|
+
"app.py — из старого шаблона streamlit, без входа по почте. Возьмите require_access() из нового "
|
|
110
|
+
"app.py и magic_link.py рядом с ним: %s (или выдайте личную ссылку: small access add ИМЯ)")
|
|
111
|
+
% (TEMPLATES_DIR / "streamlit"))
|
|
112
|
+
for name, template in (("magic_link.py", "streamlit" if streamlit else "python-http"),
|
|
113
|
+
("magic-link.js", "node-http")):
|
|
100
114
|
path = root / name
|
|
101
115
|
if path.is_file():
|
|
102
|
-
|
|
116
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
117
|
+
if "ACCESS_EMAILS" in text and (not streamlit or "MagicLinkGate" in text):
|
|
103
118
|
return None
|
|
104
119
|
return (tr("%s in the app is an old version and does not support sign-in by email. "
|
|
105
120
|
"Replace it with the new one: %s",
|
|
106
121
|
"%s в приложении старой версии и не знает входа по почте. Замените его новым: %s")
|
|
107
122
|
% (name, TEMPLATES_DIR / template / name))
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"в шаблоне streamlit вход по почте пока не поддерживается — "
|
|
113
|
-
"выдайте личную ссылку: small access add ИМЯ")
|
|
123
|
+
if streamlit:
|
|
124
|
+
return (tr("magic_link.py is missing next to app.py. Copy it from the template: %s",
|
|
125
|
+
"рядом с app.py нет magic_link.py. Скопируйте его из шаблона: %s")
|
|
126
|
+
% (TEMPLATES_DIR / "streamlit" / "magic_link.py"))
|
|
114
127
|
return None
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Collecting project files with .gitignore/.railwayignore applied, and packing them into a .tar.gz for Railway."""
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import tarfile
|
|
7
7
|
|
|
8
|
-
#
|
|
8
|
+
# Never uploaded, even without a .gitignore.
|
|
9
9
|
ALWAYS_IGNORED = {".git", "node_modules", "__pycache__", ".venv", "venv"}
|
|
10
10
|
IGNORE_FILES = (".gitignore", ".railwayignore")
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class IgnoreRules:
|
|
14
|
-
"""
|
|
14
|
+
"""A subset of .gitignore syntax: *, **, ?, [...], !negation, /anchor, trailing / for folders.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Only the files in the project root are read; nested .gitignore files are not taken into account.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
def __init__(self, lines):
|
|
@@ -90,9 +90,9 @@ def _translate(pattern):
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def collect_files(root, skip_root_files=()):
|
|
93
|
-
"""
|
|
93
|
+
"""Relative paths (with /) of all files that will go to Railway.
|
|
94
94
|
|
|
95
|
-
skip_root_files
|
|
95
|
+
skip_root_files: names of files that are not uploaded, but only when they are in the root (the CLI's own files).
|
|
96
96
|
"""
|
|
97
97
|
rules = IgnoreRules.load(root)
|
|
98
98
|
files = []
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""small
|
|
1
|
+
"""small: micro-apps on Railway with one command.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Entry point: argument parsing and error handling. The commands themselves live in small_cli/commands/,
|
|
4
|
+
shared helpers in small_cli/common.py.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
@@ -31,9 +31,10 @@ def main(argv=None):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def setup_streams():
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
"""Python writes Unicode to a terminal on its own. Output to a pipe or file
|
|
35
|
+
(`small logs | Select-String <Cyrillic word>`) it encodes as ANSI (cp1251), while PowerShell reads it in the
|
|
36
|
+
console encoding (866 or UTF-8): Cyrillic turns into garbage and searching for it finds nothing. So for a pipe
|
|
37
|
+
we write in the encoding it will be read with."""
|
|
37
38
|
encoding = pipe_encoding()
|
|
38
39
|
for stream in (sys.stdout, sys.stderr):
|
|
39
40
|
if not hasattr(stream, "reconfigure"):
|
|
@@ -45,8 +46,8 @@ def setup_streams():
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def pipe_encoding():
|
|
48
|
-
"""
|
|
49
|
-
|
|
49
|
+
"""Encoding for output to a pipe: PYTHONIOENCODING if set (small.cmd runs `python -I`, so Python itself
|
|
50
|
+
does not read it), otherwise on Windows the console encoding. None means leave it as is."""
|
|
50
51
|
explicit = os.environ.get("PYTHONIOENCODING", "").split(":")[0]
|
|
51
52
|
if explicit:
|
|
52
53
|
try:
|
|
@@ -55,7 +56,7 @@ def pipe_encoding():
|
|
|
55
56
|
return None
|
|
56
57
|
if sys.platform != "win32":
|
|
57
58
|
return None
|
|
58
|
-
codepage = _console_codepage() # 0
|
|
59
|
+
codepage = _console_codepage() # 0: the process has no console
|
|
59
60
|
return "utf-8" if codepage == 65001 else "cp%d" % codepage if codepage else None
|
|
60
61
|
|
|
61
62
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""small commands. Each module adds its subcommands with a register(subparsers) function.
|
|
2
|
+
|
|
3
|
+
The order here is the order in `small --help`: from creating an app to deleting it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from . import access, dashboard, db, deploy, destroy, domain, env, init, link, logs, mail, mcp, notify, sleep
|
|
7
|
+
|
|
8
|
+
ALL = (init, deploy, link, access, mail, notify, env, db, domain, sleep, logs, dashboard, mcp, destroy)
|