terse-cli 0.7.0 → 0.8.1
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.
- package/README.md +52 -6
- package/dist/commands/actor.d.ts.map +1 -1
- package/dist/commands/actor.js +10 -4
- package/dist/commands/actor.js.map +1 -1
- package/dist/commands/actorGenerate.d.ts +2 -1
- package/dist/commands/actorGenerate.d.ts.map +1 -1
- package/dist/commands/actorGenerate.js +17 -5
- package/dist/commands/actorGenerate.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +12 -35
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +35 -26
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +2 -2
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +4 -10
- package/dist/commands/install.js.map +1 -1
- package/dist/index.js +14 -8
- package/dist/index.js.map +1 -1
- package/dist/productFeatures.d.ts +6 -2
- package/dist/productFeatures.d.ts.map +1 -1
- package/dist/productFeatures.js +8 -1
- package/dist/productFeatures.js.map +1 -1
- package/dist/providers/typescript/actorCodegen.d.ts.map +1 -1
- package/dist/providers/typescript/actorCodegen.js +2 -0
- package/dist/providers/typescript/actorCodegen.js.map +1 -1
- package/dist/providers/typescript/actorSources.d.ts +2 -1
- package/dist/providers/typescript/actorSources.d.ts.map +1 -1
- package/dist/providers/typescript/actorSources.js +3 -0
- package/dist/providers/typescript/actorSources.js.map +1 -1
- package/dist/pythonActors.d.ts +7 -0
- package/dist/pythonActors.d.ts.map +1 -0
- package/dist/pythonActors.js +57 -0
- package/dist/pythonActors.js.map +1 -0
- package/dist/setup.d.ts +24 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +63 -0
- package/dist/setup.js.map +1 -0
- package/dist/setupTools.d.ts +11 -0
- package/dist/setupTools.d.ts.map +1 -0
- package/dist/setupTools.js +94 -0
- package/dist/setupTools.js.map +1 -0
- package/dist/sourceArchive.d.ts +11 -0
- package/dist/sourceArchive.d.ts.map +1 -0
- package/dist/sourceArchive.js +29 -0
- package/dist/sourceArchive.js.map +1 -0
- package/dist/verifiedDownload.d.ts +7 -0
- package/dist/verifiedDownload.d.ts.map +1 -0
- package/dist/verifiedDownload.js +15 -0
- package/dist/verifiedDownload.js.map +1 -0
- package/package.json +5 -5
- package/templates/python/generate.py +52 -0
- package/templates/python/init/actor.py.hbs +37 -0
- package/templates/python/init/gitignore.hbs +7 -0
- package/templates/python/init/pyproject.toml.hbs +11 -0
- package/templates/python/transport.py +171 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { CliError } from "./cliError.js";
|
|
7
|
+
const uvInstallerDigests = {
|
|
8
|
+
sh: "bd9a2739c49251c71fd3706ac00b1bb8582ea138433c6e52840de4aba646e46a",
|
|
9
|
+
ps1: "87c68faa52d996e5e2d6606ede188b89a1fdbfdf4c4ee1da3d0c24a273881935"
|
|
10
|
+
};
|
|
11
|
+
export class LocalSetupTools {
|
|
12
|
+
downloads;
|
|
13
|
+
constructor(downloads) {
|
|
14
|
+
this.downloads = downloads;
|
|
15
|
+
}
|
|
16
|
+
async available(tool) {
|
|
17
|
+
configureToolPaths();
|
|
18
|
+
try {
|
|
19
|
+
if (tool === "node")
|
|
20
|
+
return atLeast(process.versions.node, [22, 19, 0]);
|
|
21
|
+
if (tool === "python") {
|
|
22
|
+
const version = await run("uv", ["python", "find", "--no-python-downloads", "--show-version", process.env.DURABLE_ACTORS_PYTHON || ">=3.11"]);
|
|
23
|
+
return atLeast(version, [3, 11, 0]);
|
|
24
|
+
}
|
|
25
|
+
const version = await run(tool, ["--version"]);
|
|
26
|
+
if (tool === "uv")
|
|
27
|
+
return atLeast(version.replace(/^uv\s+/, ""), [0, 6, 13]);
|
|
28
|
+
return tool === "bun" ? atLeast(version, [1, 4, 2]) : Boolean(version.trim());
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async install(tool) {
|
|
35
|
+
log.step(`Installing ${tool}`);
|
|
36
|
+
if (tool === "uv")
|
|
37
|
+
await this.installUv();
|
|
38
|
+
else if (tool === "python")
|
|
39
|
+
await run("uv", ["python", "install", process.env.DURABLE_ACTORS_PYTHON || "3.13"]);
|
|
40
|
+
else if (tool === "bun")
|
|
41
|
+
await run("npm", ["install", "--global", "bun@1.4.2"]);
|
|
42
|
+
else
|
|
43
|
+
throw new CliError("unsupported_installation", `Install ${tool} using the Node.js installer.`);
|
|
44
|
+
configureToolPaths();
|
|
45
|
+
}
|
|
46
|
+
async installUv() {
|
|
47
|
+
const windows = process.platform === "win32";
|
|
48
|
+
const extension = windows ? "ps1" : "sh";
|
|
49
|
+
const url = `https://github.com/astral-sh/uv/releases/download/0.11.31/uv-installer.${extension}`;
|
|
50
|
+
const installer = await this.downloads.download(url, uvInstallerDigests[extension]);
|
|
51
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "terse-uv-install-"));
|
|
52
|
+
try {
|
|
53
|
+
const file = path.join(directory, windows ? "install.ps1" : "install.sh");
|
|
54
|
+
await writeFile(file, installer);
|
|
55
|
+
const env = { ...process.env, UV_INSTALL_DIR: uvDirectory() };
|
|
56
|
+
if (windows)
|
|
57
|
+
await run("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", file], env);
|
|
58
|
+
else
|
|
59
|
+
await run("sh", [file], env);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await rm(directory, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function configureToolPaths() {
|
|
67
|
+
const key = Object.keys(process.env).find(name => name.toLowerCase() === "path") ?? "PATH";
|
|
68
|
+
const entries = (process.env[key] ?? "").split(path.delimiter);
|
|
69
|
+
const bun = path.join(process.env.BUN_INSTALL || path.join(os.homedir(), ".bun"), "bin");
|
|
70
|
+
process.env[key] = [...new Set([uvDirectory(), ...entries, bun])].join(path.delimiter);
|
|
71
|
+
}
|
|
72
|
+
function uvDirectory() {
|
|
73
|
+
return process.env.UV_INSTALL_DIR || path.join(os.homedir(), ".local", "bin");
|
|
74
|
+
}
|
|
75
|
+
function run(command, args, env = process.env) {
|
|
76
|
+
const cleanEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.toLowerCase().startsWith("npm_config_")));
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
execFile(command, args, { env: cleanEnv, shell: process.platform === "win32" && command === "npm", timeout: 120000, maxBuffer: 8 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
79
|
+
if (error)
|
|
80
|
+
reject(new CliError("tool_installation_failed", `${command} ${args.join(" ")} failed.`, { detail: stderr || error.message }));
|
|
81
|
+
else
|
|
82
|
+
resolve(stdout);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function atLeast(version, minimum) {
|
|
87
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
88
|
+
if (!match)
|
|
89
|
+
return false;
|
|
90
|
+
const numbers = match.slice(1).map(Number);
|
|
91
|
+
const difference = numbers.findIndex((value, index) => value !== minimum[index]);
|
|
92
|
+
return difference < 0 || numbers[difference] > minimum[difference];
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=setupTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setupTools.js","sourceRoot":"","sources":["../src/setupTools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Exclusions {
|
|
2
|
+
dirs: ReadonlySet<string>;
|
|
3
|
+
files: ReadonlySet<string>;
|
|
4
|
+
}
|
|
5
|
+
export declare function buildSourceArchive(project: string, exclusions: Exclusions): {
|
|
6
|
+
zipBytes: Uint8Array;
|
|
7
|
+
fileCount: number;
|
|
8
|
+
zipSizeBytes: number;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=sourceArchive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sourceArchive.d.ts","sourceRoot":"","sources":["../src/sourceArchive.ts"],"names":[],"mappings":"AAMA,UAAU,UAAU;IAChB,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACzB,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC7B;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG;IAAE,QAAQ,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAM7I"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { zipSync } from "fflate";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CliError } from "./cliError.js";
|
|
5
|
+
const pythonDirectories = new Set([".venv", "venv", "__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache"]);
|
|
6
|
+
export function buildSourceArchive(project, exclusions) {
|
|
7
|
+
const files = collectFiles(project, project, exclusions);
|
|
8
|
+
const fileCount = Object.keys(files).length;
|
|
9
|
+
if (fileCount === 0)
|
|
10
|
+
throw new CliError("no_files_to_deploy", "No files found to deploy");
|
|
11
|
+
const zipBytes = zipSync(files, { level: 6 });
|
|
12
|
+
return { zipBytes, fileCount, zipSizeBytes: zipBytes.length };
|
|
13
|
+
}
|
|
14
|
+
function collectFiles(directory, project, exclusions) {
|
|
15
|
+
const files = {};
|
|
16
|
+
if (fs.existsSync(path.join(directory, "pyvenv.cfg")))
|
|
17
|
+
return files;
|
|
18
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
19
|
+
if (exclusions.dirs.has(entry.name) || exclusions.files.has(entry.name) || pythonDirectories.has(entry.name))
|
|
20
|
+
continue;
|
|
21
|
+
const source = path.join(directory, entry.name);
|
|
22
|
+
if (entry.isDirectory())
|
|
23
|
+
Object.assign(files, collectFiles(source, project, exclusions));
|
|
24
|
+
else if (entry.isFile())
|
|
25
|
+
files[path.relative(project, source).split(path.sep).join("/")] = new Uint8Array(fs.readFileSync(source));
|
|
26
|
+
}
|
|
27
|
+
return files;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=sourceArchive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sourceArchive.js","sourceRoot":"","sources":["../src/sourceArchive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAChC,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAOxC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,CAAC,CAAC,CAAA;AAElH,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,UAAsB;IACtE,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAA;IACxD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAA;IAC3C,IAAI,SAAS,KAAK,CAAC;QAAE,MAAM,IAAI,QAAQ,CAAC,oBAAoB,EAAE,0BAA0B,CAAC,CAAA;IACzF,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;IAC7C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;AACjE,CAAC;AAED,SAAS,YAAY,CAAC,SAAiB,EAAE,OAAe,EAAE,UAAsB;IAC5E,MAAM,KAAK,GAA+B,EAAE,CAAA;IAC5C,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IACnE,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACrE,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,SAAQ;QACtH,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QAC/C,IAAI,KAAK,CAAC,WAAW,EAAE;YAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAA;aACnF,IAAI,KAAK,CAAC,MAAM,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAA;IACtI,CAAC;IACD,OAAO,KAAK,CAAA;AAChB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface VerifiedDownloader {
|
|
2
|
+
download(url: string, sha256: string): Promise<Buffer>;
|
|
3
|
+
}
|
|
4
|
+
export declare class HttpVerifiedDownloader implements VerifiedDownloader {
|
|
5
|
+
download(url: string, sha256: string): Promise<Buffer>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=verifiedDownload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifiedDownload.d.ts","sourceRoot":"","sources":["../src/verifiedDownload.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACzD;AAED,qBAAa,sBAAuB,YAAW,kBAAkB;IACvD,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAS/D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliError } from "./cliError.js";
|
|
3
|
+
export class HttpVerifiedDownloader {
|
|
4
|
+
async download(url, sha256) {
|
|
5
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
6
|
+
if (!response.ok)
|
|
7
|
+
throw new CliError("installer_download_failed", `Could not download the installer: HTTP ${response.status}.`, { url });
|
|
8
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
9
|
+
if (createHash("sha256").update(bytes).digest("hex") !== sha256) {
|
|
10
|
+
throw new CliError("installer_checksum_mismatch", "Installer checksum verification failed.", { url });
|
|
11
|
+
}
|
|
12
|
+
return bytes;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=verifiedDownload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifiedDownload.js","sourceRoot":"","sources":["../src/verifiedDownload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAMxC,MAAM,OAAO,sBAAsB;IAC/B,KAAK,CAAC,QAAQ,CAAC,GAAW,EAAE,MAAc;QACtC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAC1E,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,QAAQ,CAAC,2BAA2B,EAAE,0CAA0C,QAAQ,CAAC,MAAM,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,CAAA;QACxI,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;QACvD,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,EAAE,CAAC;YAC9D,MAAM,IAAI,QAAQ,CAAC,6BAA6B,EAAE,yCAAyC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAA;QACzG,CAAC;QACD,OAAO,KAAK,CAAA;IAChB,CAAC;CACJ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"handlebars": "^4.7.9",
|
|
33
33
|
"import-meta-resolve": "^4.2.0",
|
|
34
34
|
"json-schema-to-typescript": "^15.0.4",
|
|
35
|
-
"durable-actors": "0.5.
|
|
35
|
+
"durable-actors": "0.5.5",
|
|
36
36
|
"little-durable": "0.1.3",
|
|
37
37
|
"luxon": "^3.7.2",
|
|
38
38
|
"ora": "^8.0.0",
|
|
39
39
|
"tsx": "^4.0.0",
|
|
40
40
|
"typescript": "^5.0.0",
|
|
41
41
|
"zod": "^4.3.6",
|
|
42
|
-
"terse-
|
|
43
|
-
"terse-
|
|
42
|
+
"terse-sdk": "0.8.1",
|
|
43
|
+
"terse-types": "0.8.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/luxon": "^3.7.1",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "rm -rf dist && tsc && chmod +x dist/index.js",
|
|
51
51
|
"dev": "tsc --watch --preserveWatchOutput",
|
|
52
|
-
"test": "node --import tsx --test test/**/*.test.ts",
|
|
52
|
+
"test": "node --import tsx --test test/**/*.test.ts tests/*.test.ts",
|
|
53
53
|
"test:actors:local": "TERSE_TEST_LOCAL_ACTORS=1 node --import tsx --test test/localActorRuntime.test.ts",
|
|
54
54
|
"format": "pnpm exec prettier --config ../../.prettierrc --write \"src/**/*.ts\"",
|
|
55
55
|
"format:check": "pnpm exec prettier --config ../../.prettierrc --check \"src/**/*.ts\"",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Generate Python actor clients with Terse's managed connection defaults."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from durable_actors.codegen import generate_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate(output: Path) -> None:
|
|
12
|
+
contract = json.load(sys.stdin)
|
|
13
|
+
generate_client(contract, output)
|
|
14
|
+
module = "_terse_transport"
|
|
15
|
+
while any(
|
|
16
|
+
"_" + actor["actorName"].lower() == module for actor in contract["actors"]
|
|
17
|
+
):
|
|
18
|
+
module += "_"
|
|
19
|
+
files = [
|
|
20
|
+
output / "__init__.py",
|
|
21
|
+
output / "actors.py",
|
|
22
|
+
*(
|
|
23
|
+
output / ("_" + actor["actorName"].lower() + ".py")
|
|
24
|
+
for actor in contract["actors"]
|
|
25
|
+
),
|
|
26
|
+
]
|
|
27
|
+
for file in files:
|
|
28
|
+
tree = ast.parse(file.read_text())
|
|
29
|
+
for index, node in enumerate(tree.body):
|
|
30
|
+
if not isinstance(node, ast.ImportFrom):
|
|
31
|
+
continue
|
|
32
|
+
names = {
|
|
33
|
+
"durable_actors.client": "default_client",
|
|
34
|
+
"durable_actors.proxy": "prepare_authorization",
|
|
35
|
+
}
|
|
36
|
+
replacement = names.get(node.module or "")
|
|
37
|
+
selected = [name for name in node.names if name.name == replacement]
|
|
38
|
+
if selected:
|
|
39
|
+
node.names = [name for name in node.names if name not in selected]
|
|
40
|
+
injected = ast.ImportFrom(module=module, names=selected, level=1)
|
|
41
|
+
if node.names:
|
|
42
|
+
tree.body.insert(index + 1, injected)
|
|
43
|
+
else:
|
|
44
|
+
tree.body[index] = injected
|
|
45
|
+
file.write_text(ast.unparse(ast.fix_missing_locations(tree)) + "\n")
|
|
46
|
+
(output / (module + ".py")).write_text(
|
|
47
|
+
Path(__file__).with_name("transport.py").read_text()
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
generate(Path(sys.argv[1]))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from durable_actors import Actor, ActorSocket, emitted
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Member(BaseModel):
|
|
6
|
+
"""Metadata passed in when connecting via websocket"""
|
|
7
|
+
|
|
8
|
+
name: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SendMessage(BaseModel):
|
|
12
|
+
"""Message sent to Durable Actor through websocket"""
|
|
13
|
+
|
|
14
|
+
text: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Message(BaseModel):
|
|
18
|
+
"""Persisted message payload"""
|
|
19
|
+
|
|
20
|
+
author: Member
|
|
21
|
+
text: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Chat(Actor[Member, SendMessage, Message]):
|
|
25
|
+
"""A Durable Actor chatroom"""
|
|
26
|
+
|
|
27
|
+
messages: list[Message] = emitted(default_factory=list)
|
|
28
|
+
|
|
29
|
+
def post(self, author: Member, text: str) -> Message:
|
|
30
|
+
"""Save a message and broadcast it to connected members."""
|
|
31
|
+
message = Message(author=author, text=text)
|
|
32
|
+
self.messages.append(message)
|
|
33
|
+
self.broadcast(message)
|
|
34
|
+
return message
|
|
35
|
+
|
|
36
|
+
def on_message(self, socket: ActorSocket[Member, Message], message: SendMessage) -> None:
|
|
37
|
+
self.post(socket.metadata, message.text)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Terse connection defaults for this generated actor package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from functools import cache
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import unquote, urlsplit
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from durable_actors import (
|
|
14
|
+
ActorSession,
|
|
15
|
+
ActorSessionRejectedError,
|
|
16
|
+
ActorSessionTransport,
|
|
17
|
+
Client,
|
|
18
|
+
SocketGrant,
|
|
19
|
+
)
|
|
20
|
+
from durable_actors.client import ActorTransport
|
|
21
|
+
from durable_actors.proxy import (
|
|
22
|
+
SocketAuthorization,
|
|
23
|
+
prepare_authorization as issue_authorization,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cache
|
|
28
|
+
def default_client() -> ActorTransport:
|
|
29
|
+
"""Use TERSE_ACTOR_URL and TERSE_API_KEY, with local development defaults."""
|
|
30
|
+
value = (
|
|
31
|
+
os.environ.get("TERSE_ACTOR_URL", "").strip()
|
|
32
|
+
or "http://127.0.0.1:7100/v1/projects/local/actors"
|
|
33
|
+
)
|
|
34
|
+
url = urlsplit(value)
|
|
35
|
+
match = re.fullmatch(r"/(?:(sdk|v1)/projects/)?([^/]+)/actors/?", url.path)
|
|
36
|
+
if (
|
|
37
|
+
not match
|
|
38
|
+
or url.scheme not in {"http", "https"}
|
|
39
|
+
or not url.hostname
|
|
40
|
+
or url.username
|
|
41
|
+
or url.password
|
|
42
|
+
or url.query
|
|
43
|
+
or url.fragment
|
|
44
|
+
):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"TERSE_ACTOR_URL must end in /<projectId>/actors or /v1/projects/<projectId>/actors, without credentials, query, or fragment"
|
|
47
|
+
)
|
|
48
|
+
project_id = unquote(match[2])
|
|
49
|
+
if not re.fullmatch(r"[A-Za-z0-9._-]{1,64}", project_id) or project_id in {
|
|
50
|
+
".",
|
|
51
|
+
"..",
|
|
52
|
+
}:
|
|
53
|
+
raise ValueError("Invalid project in TERSE_ACTOR_URL")
|
|
54
|
+
origin = f"{url.scheme}://{url.netloc}"
|
|
55
|
+
key = os.environ.get("TERSE_API_KEY", "").strip()
|
|
56
|
+
runtime = match[1] == "v1"
|
|
57
|
+
if not key and not (runtime and url.hostname in {"localhost", "127.0.0.1", "::1"}):
|
|
58
|
+
raise ValueError("Missing TERSE_API_KEY for this actor project")
|
|
59
|
+
client: Client | TerseTransport
|
|
60
|
+
if runtime:
|
|
61
|
+
client = Client(origin, project_id=project_id, api_key=key or None)
|
|
62
|
+
else:
|
|
63
|
+
client = TerseTransport(
|
|
64
|
+
f"{origin}/{project_id}/actors",
|
|
65
|
+
project_id,
|
|
66
|
+
key,
|
|
67
|
+
httpx.Client(timeout=180, follow_redirects=False),
|
|
68
|
+
)
|
|
69
|
+
atexit.register(client.close)
|
|
70
|
+
return client
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def prepare_authorization(
|
|
74
|
+
authorization: SocketAuthorization[Any], transport: ActorTransport | None = None
|
|
75
|
+
) -> SocketGrant:
|
|
76
|
+
"""Issue a typed browser grant using the selected Terse project."""
|
|
77
|
+
return issue_authorization(
|
|
78
|
+
authorization, transport if transport is not None else default_client()
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TerseTransport:
|
|
83
|
+
"""Exchange Terse credentials for renewable runtime sessions and socket grants."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self, actor_url: str, project_id: str, api_key: str, http: httpx.Client
|
|
87
|
+
) -> None:
|
|
88
|
+
self._actor_url = actor_url
|
|
89
|
+
self._project_id = project_id
|
|
90
|
+
self._headers = {
|
|
91
|
+
"Authorization": f"Bearer {api_key}",
|
|
92
|
+
"Accept": "application/json",
|
|
93
|
+
}
|
|
94
|
+
self._http = http
|
|
95
|
+
self._session = ActorSessionTransport(
|
|
96
|
+
project_id=project_id,
|
|
97
|
+
get_session=self._get_session,
|
|
98
|
+
create_transport=self._session_client,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def invoke(
|
|
102
|
+
self, actor_name: str, actor_id: str, method: str, args: list[Any]
|
|
103
|
+
) -> Any:
|
|
104
|
+
"""Invoke through a renewable actor session."""
|
|
105
|
+
return self._session.invoke(actor_name, actor_id, method, args)
|
|
106
|
+
|
|
107
|
+
def broadcast(self, actor_name: str, actor_id: str, message: Any) -> None:
|
|
108
|
+
"""Broadcast through the current actor session."""
|
|
109
|
+
self._session.broadcast(actor_name, actor_id, message)
|
|
110
|
+
|
|
111
|
+
def prepare_websocket(
|
|
112
|
+
self,
|
|
113
|
+
actor_name: str,
|
|
114
|
+
actor_id: str,
|
|
115
|
+
metadata: Any,
|
|
116
|
+
*,
|
|
117
|
+
authorization_lifetime_ms: int = 900000,
|
|
118
|
+
home_region: str | None = None,
|
|
119
|
+
) -> SocketGrant:
|
|
120
|
+
"""Ask Terse for a direct WebSocket grant for this actor."""
|
|
121
|
+
response = self._http.post(
|
|
122
|
+
self._actor_url,
|
|
123
|
+
headers=self._headers,
|
|
124
|
+
json={
|
|
125
|
+
"actorName": actor_name,
|
|
126
|
+
"actorId": actor_id,
|
|
127
|
+
"metadata": metadata,
|
|
128
|
+
"authorizationLifetimeMs": authorization_lifetime_ms,
|
|
129
|
+
**({"homeRegion": home_region} if home_region is not None else {}),
|
|
130
|
+
},
|
|
131
|
+
timeout=10,
|
|
132
|
+
follow_redirects=False,
|
|
133
|
+
)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
grant = SocketGrant.model_validate(response.json(), strict=True)
|
|
136
|
+
if urlsplit(grant.websocket_url).scheme not in {"ws", "wss"}:
|
|
137
|
+
raise ValueError("Invalid WebSocket grant URL")
|
|
138
|
+
return grant
|
|
139
|
+
|
|
140
|
+
def close(self) -> None:
|
|
141
|
+
"""Stop session renewal and close this transport's HTTP client."""
|
|
142
|
+
self._session.close()
|
|
143
|
+
self._http.close()
|
|
144
|
+
|
|
145
|
+
def _get_session(self) -> ActorSession:
|
|
146
|
+
response = self._http.post(
|
|
147
|
+
self._actor_url + "/access",
|
|
148
|
+
headers=self._headers,
|
|
149
|
+
timeout=10,
|
|
150
|
+
follow_redirects=False,
|
|
151
|
+
)
|
|
152
|
+
if response.status_code in {401, 403}:
|
|
153
|
+
raise ActorSessionRejectedError(
|
|
154
|
+
f"Terse actor authorization failed (HTTP {response.status_code})"
|
|
155
|
+
)
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
value = response.json()
|
|
158
|
+
return ActorSession(
|
|
159
|
+
project_id=value["projectId"],
|
|
160
|
+
control_plane_url=value["controlPlaneUrl"],
|
|
161
|
+
token=value["token"],
|
|
162
|
+
expires_at_ms=value["expiresAtMs"],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _session_client(self, session: ActorSession) -> Client:
|
|
166
|
+
return Client(
|
|
167
|
+
session.control_plane_url,
|
|
168
|
+
project_id=session.project_id,
|
|
169
|
+
api_key=session.token,
|
|
170
|
+
http=self._http,
|
|
171
|
+
)
|