@definite-app/data-apps 1.0.0

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.
Files changed (32) hide show
  1. package/CLAUDE.md +686 -0
  2. package/LICENSE +201 -0
  3. package/README.md +643 -0
  4. package/build.mjs +459 -0
  5. package/examples/_refined_demo/app.json +15 -0
  6. package/examples/_refined_demo/data/sample.parquet +0 -0
  7. package/examples/_refined_demo/gen_preview_data.py +59 -0
  8. package/examples/_refined_demo/preview-data.json +13 -0
  9. package/examples/_refined_demo/src/App.tsx +188 -0
  10. package/examples/_refined_demo/src/main.tsx +12 -0
  11. package/examples/loan-portfolio/app.json +31 -0
  12. package/examples/loan-portfolio/data/loan_book.parquet +0 -0
  13. package/examples/loan-portfolio/gen_preview_data.py +454 -0
  14. package/examples/loan-portfolio/preview-data.json +84 -0
  15. package/examples/loan-portfolio/src/App.tsx +1103 -0
  16. package/examples/loan-portfolio/src/main.tsx +12 -0
  17. package/examples/revenue-explorer/app.json +23 -0
  18. package/examples/revenue-explorer/data/transactions.parquet +0 -0
  19. package/examples/revenue-explorer/gen_preview_data.py +129 -0
  20. package/examples/revenue-explorer/preview-data.json +49 -0
  21. package/examples/revenue-explorer/src/App.tsx +527 -0
  22. package/examples/revenue-explorer/src/main.tsx +12 -0
  23. package/package.json +55 -0
  24. package/preview.mjs +35 -0
  25. package/runtime/definite-runtime.tsx +5934 -0
  26. package/scripts/headless-smoke.mjs +196 -0
  27. package/templates/blank/app.json +15 -0
  28. package/templates/blank/src/App.tsx +41 -0
  29. package/templates/blank/src/main.tsx +12 -0
  30. package/templates/refined/app.json +15 -0
  31. package/templates/refined/src/App.tsx +198 -0
  32. package/templates/refined/src/main.tsx +12 -0
package/build.mjs ADDED
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { build } from "esbuild";
7
+
8
+ const buildMjsDir = path.dirname(fileURLToPath(import.meta.url));
9
+ const toolkitNodeModules = path.join(buildMjsDir, "node_modules");
10
+ const canonicalRuntimePath = path.join(buildMjsDir, "runtime", "definite-runtime.tsx");
11
+
12
+ const definiteRuntimeAliasPlugin = {
13
+ name: "definite-runtime-alias",
14
+ setup(build) {
15
+ build.onResolve({ filter: /^@definite\/runtime$/ }, () => ({
16
+ path: canonicalRuntimePath,
17
+ }));
18
+ },
19
+ };
20
+
21
+ function usage() {
22
+ console.error("Usage: node build.mjs <app-dir> [--preview-data /path/to/preview-data.json]");
23
+ process.exit(1);
24
+ }
25
+
26
+ function escapeInlineScript(value) {
27
+ return value.replace(/<\/script/gi, "<\\/script");
28
+ }
29
+
30
+ async function collectSourceFiles(dir) {
31
+ const entries = await readdir(dir, { withFileTypes: true });
32
+ const files = [];
33
+
34
+ for (const entry of entries) {
35
+ const fullPath = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ if (entry.name === "dist" || entry.name === "node_modules" || entry.name.startsWith(".")) {
38
+ continue;
39
+ }
40
+ files.push(...await collectSourceFiles(fullPath));
41
+ continue;
42
+ }
43
+
44
+ if (!/\.(jsx?|tsx?)$/.test(entry.name)) {
45
+ continue;
46
+ }
47
+
48
+ files.push(fullPath);
49
+ }
50
+
51
+ return files;
52
+ }
53
+
54
+ function getLineNumber(source, index) {
55
+ return source.slice(0, index).split("\n").length;
56
+ }
57
+
58
+ function parseLiteralString(argumentSource) {
59
+ const trimmed = argumentSource.trim();
60
+ const match = /^(["'`])(?<value>[\s\S]*?)\1$/u.exec(trimmed);
61
+ if (!match?.groups) {
62
+ return null;
63
+ }
64
+ return match.groups.value;
65
+ }
66
+
67
+ function findHookCalls(source) {
68
+ const calls = [];
69
+ const pattern = /\b(useDataset|useJsonResource)\s*\(([\s\S]*?)\)/g;
70
+
71
+ for (const match of source.matchAll(pattern)) {
72
+ const fullMatch = match[0];
73
+ const hookName = match[1];
74
+ const args = match[2] ?? "";
75
+
76
+ // Skip the hook function definitions in the runtime file.
77
+ const definitionPrefix = source.slice(Math.max(0, match.index - 24), match.index);
78
+ if (/\b(?:export\s+)?function\s*$/.test(definitionPrefix)) {
79
+ continue;
80
+ }
81
+
82
+ const firstArg = args.split(",")[0] ?? "";
83
+ calls.push({
84
+ hookName,
85
+ raw: fullMatch,
86
+ firstArg,
87
+ index: match.index ?? 0,
88
+ });
89
+ }
90
+
91
+ return calls;
92
+ }
93
+
94
+ const REQUIRED_FILTERS_MARKER_RE = /\{\{\s*required_filters\s*\}\}/;
95
+
96
+ function validateManifest(manifest, manifestPath) {
97
+ if (manifest.version !== 2) {
98
+ throw new Error(`Expected ${manifestPath} to contain {"version": 2}`);
99
+ }
100
+
101
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
102
+ throw new Error(`Expected ${manifestPath} to contain a JSON object`);
103
+ }
104
+
105
+ if (typeof manifest.entry !== "string" || manifest.entry.length === 0) {
106
+ throw new Error(`Expected ${manifestPath} to define a non-empty "entry"`);
107
+ }
108
+
109
+ if (!manifest.resources || typeof manifest.resources !== "object" || Array.isArray(manifest.resources)) {
110
+ throw new Error(`Expected ${manifestPath} to define a "resources" object`);
111
+ }
112
+
113
+ const validKinds = new Set(["dataset", "json"]);
114
+ for (const [key, resource] of Object.entries(manifest.resources)) {
115
+ if (!key || typeof key !== "string") {
116
+ throw new Error(`Expected all resource keys in ${manifestPath} to be non-empty strings`);
117
+ }
118
+ if (!resource || typeof resource !== "object" || Array.isArray(resource)) {
119
+ throw new Error(`Resource ${key} in ${manifestPath} must be an object`);
120
+ }
121
+ if (!validKinds.has(resource.kind)) {
122
+ throw new Error(`Resource ${key} in ${manifestPath} has unsupported kind "${resource.kind}"`);
123
+ }
124
+
125
+ const bindings = resource.requiredFilters;
126
+ if (bindings === undefined || bindings === null) {
127
+ continue;
128
+ }
129
+ if (typeof bindings !== "object" || Array.isArray(bindings)) {
130
+ throw new Error(`Resource ${key} in ${manifestPath}: requiredFilters must be an object`);
131
+ }
132
+ for (const [member, binding] of Object.entries(bindings)) {
133
+ if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
134
+ throw new Error(
135
+ `Resource ${key} in ${manifestPath}: requiredFilters[${member}] must be an object with a "column" field`,
136
+ );
137
+ }
138
+ if (typeof binding.column !== "string" || binding.column.length === 0) {
139
+ throw new Error(
140
+ `Resource ${key} in ${manifestPath}: requiredFilters[${member}].column must be a non-empty string`,
141
+ );
142
+ }
143
+ }
144
+
145
+ // SQL resources with requiredFilters MUST contain the {{ required_filters }} marker.
146
+ // Cube resources don't need the marker — Cube filter members are dimension names and
147
+ // get merged server-side by the query handler.
148
+ const source = resource.source;
149
+ if (source && typeof source === "object" && source.type === "sql") {
150
+ if (typeof source.sql === "string" && !REQUIRED_FILTERS_MARKER_RE.test(source.sql)) {
151
+ throw new Error(
152
+ `Resource ${key} in ${manifestPath} declares requiredFilters but its SQL is missing the ` +
153
+ `{{ required_filters }} marker. Add it to a WHERE/AND clause where the tenant filter should land.`,
154
+ );
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ async function validateResourceHookUsage(appDir, manifest) {
161
+ const srcDir = path.join(appDir, "src");
162
+ const sourceFiles = await collectSourceFiles(srcDir);
163
+ const errors = [];
164
+ const resources = manifest.resources ?? {};
165
+
166
+ for (const filePath of sourceFiles) {
167
+ const source = await readFile(filePath, "utf8");
168
+ const calls = findHookCalls(source);
169
+
170
+ for (const call of calls) {
171
+ const line = getLineNumber(source, call.index);
172
+ const literalKey = parseLiteralString(call.firstArg);
173
+ const relativePath = path.relative(appDir, filePath);
174
+ const expectedKind = call.hookName === "useDataset" ? "dataset" : "json";
175
+
176
+ if (!literalKey) {
177
+ errors.push(
178
+ `${relativePath}:${line} ${call.hookName}() must use a literal manifest key string as its first argument.`,
179
+ );
180
+ continue;
181
+ }
182
+
183
+ const resource = resources[literalKey];
184
+ if (!resource) {
185
+ errors.push(
186
+ `${relativePath}:${line} ${call.hookName}("${literalKey}") does not match any key in app.json resources.`,
187
+ );
188
+ continue;
189
+ }
190
+
191
+ if (resource.kind !== expectedKind) {
192
+ errors.push(
193
+ `${relativePath}:${line} ${call.hookName}("${literalKey}") expects a ${expectedKind} resource, but app.json declares "${literalKey}" as ${resource.kind}.`,
194
+ );
195
+ }
196
+ }
197
+ }
198
+
199
+ if (errors.length > 0) {
200
+ throw new Error(
201
+ [
202
+ "Invalid data-apps-v2 app. Resource hooks must use literal keys that exist in app.json.",
203
+ ...errors,
204
+ ].join("\n"),
205
+ );
206
+ }
207
+ }
208
+
209
+ const args = process.argv.slice(2);
210
+ const appDir = args[0] ? path.resolve(args[0]) : "";
211
+ if (!appDir) {
212
+ usage();
213
+ }
214
+
215
+ let previewDataPath = null;
216
+ for (let index = 1; index < args.length; index += 1) {
217
+ if (args[index] === "--preview-data") {
218
+ previewDataPath = args[index + 1] ? path.resolve(args[index + 1]) : null;
219
+ index += 1;
220
+ }
221
+ }
222
+
223
+ const manifestPath = path.join(appDir, "app.json");
224
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
225
+ validateManifest(manifest, manifestPath);
226
+ await validateResourceHookUsage(appDir, manifest);
227
+ const previewData = previewDataPath ? JSON.parse(await readFile(previewDataPath, "utf8")) : null;
228
+ if (previewData?.datasets) {
229
+ const previewBaseDir = path.dirname(previewDataPath);
230
+ for (const [key, value] of Object.entries(previewData.datasets)) {
231
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof value.file === "string") {
232
+ const filePath = path.resolve(previewBaseDir, value.file);
233
+ let bytes;
234
+ try {
235
+ bytes = await readFile(filePath);
236
+ }
237
+ catch (error) {
238
+ throw new Error(
239
+ `Preview dataset "${key}" references file "${value.file}" (resolved: ${filePath}) which could not be read: ${
240
+ error instanceof Error ? error.message : String(error)
241
+ }`,
242
+ );
243
+ }
244
+ const format = value.format ?? (filePath.endsWith(".duckdb") ? "duckdb" : "parquet");
245
+ previewData.datasets[key] = {
246
+ format,
247
+ base64: bytes.toString("base64"),
248
+ };
249
+ }
250
+ }
251
+ }
252
+
253
+ const entry = path.join(appDir, manifest.entry ?? "src/main.tsx");
254
+ const outDir = path.join(appDir, "dist");
255
+ await mkdir(outDir, { recursive: true });
256
+
257
+ const result = await build({
258
+ absWorkingDir: appDir,
259
+ entryPoints: [entry],
260
+ bundle: true,
261
+ write: false,
262
+ format: "esm",
263
+ platform: "browser",
264
+ target: ["chrome123", "safari17", "firefox124"],
265
+ jsx: "automatic",
266
+ legalComments: "none",
267
+ sourcemap: "inline",
268
+ // nodePaths lets esbuild resolve react/react-dom from the toolkit's own
269
+ // node_modules even when appDir lives outside this repo (e.g., a user
270
+ // scaffolds an app anywhere on disk and runs `node <toolkit>/build.mjs <app>`).
271
+ nodePaths: [toolkitNodeModules],
272
+ plugins: [definiteRuntimeAliasPlugin],
273
+ define: {
274
+ "process.env.NODE_ENV": JSON.stringify("production"),
275
+ },
276
+ });
277
+
278
+ const jsFile = result.outputFiles.find((file) => file.path.endsWith(".js")) ?? result.outputFiles[0];
279
+ if (!jsFile) {
280
+ throw new Error("esbuild did not emit a JavaScript bundle");
281
+ }
282
+
283
+ const title = typeof manifest.name === "string" && manifest.name.length > 0
284
+ ? manifest.name
285
+ : "Definite Data App";
286
+ const importMap = {
287
+ imports: {
288
+ "apache-arrow": "https://storage.googleapis.com/definite-public/libs/apache-arrow@17.0.0/apache-arrow.esm.js",
289
+ },
290
+ };
291
+
292
+ const html = `<!DOCTYPE html>
293
+ <html lang="en">
294
+ <head>
295
+ <meta charset="UTF-8" />
296
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
297
+ <meta name="definite-app-version" content="2" />
298
+ <link rel="icon" href="data:," />
299
+ <title>${title}</title>
300
+ <script src="https://cdn.tailwindcss.com"></script>
301
+ <script src="https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js"></script>
302
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
303
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
304
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
305
+ <link rel="preload" href="https://cdn.jsdelivr.net/npm/@perspective-dev/server@4.3.0/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
306
+ <link rel="preload" href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer@4.3.0/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
307
+ <link rel="stylesheet" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer@4.3.0/dist/css/themes.css" />
308
+ <script type="importmap">${escapeInlineScript(JSON.stringify(importMap))}</script>
309
+ <style>
310
+ :root {
311
+ color-scheme: dark;
312
+ --bg-primary: #09090b;
313
+ --bg-card: #0f0f12;
314
+ --bg-elevated: #16161a;
315
+ --bg-hover: #1c1c22;
316
+ --border: #1e1e26;
317
+ --border-hover: #2e2e38;
318
+ --text-primary: #ececef;
319
+ --text-secondary: #9898a0;
320
+ --text-muted: #5c5c66;
321
+ --accent: #0A99FF;
322
+ --accent-muted: rgba(10, 153, 255, 0.15);
323
+ --accent-subtle: rgba(10, 153, 255, 0.06);
324
+ --accent-strong: #38bdf8;
325
+ --ring: rgba(255,255,255,0.06);
326
+ --shadow-card: 0 1px 2px rgba(0,0,0,0.4), 0 0 0 1px var(--border);
327
+ --shadow-card-hover: 0 4px 16px rgba(0,0,0,0.5), 0 0 0 1px var(--border-hover);
328
+ }
329
+ html.light {
330
+ color-scheme: light;
331
+ --bg-primary: #fafafa;
332
+ --bg-card: #ffffff;
333
+ --bg-elevated: #f4f4f5;
334
+ --bg-hover: #ebebed;
335
+ --border: #e4e4e7;
336
+ --border-hover: #c8c8ce;
337
+ --text-primary: #09090b;
338
+ --text-secondary: #52525b;
339
+ --text-muted: #a1a1aa;
340
+ --accent: #0A84D0;
341
+ --accent-muted: rgba(10, 132, 208, 0.12);
342
+ --accent-subtle: rgba(10, 132, 208, 0.04);
343
+ --accent-strong: #0284c7;
344
+ --ring: rgba(0,0,0,0.05);
345
+ --shadow-card: 0 1px 2px rgba(0,0,0,0.05), 0 0 0 1px var(--border);
346
+ --shadow-card-hover: 0 4px 12px rgba(0,0,0,0.08), 0 0 0 1px var(--border-hover);
347
+ }
348
+ * {
349
+ box-sizing: border-box;
350
+ font-family: "Inter", system-ui, sans-serif;
351
+ }
352
+ h1, h2, h3 {
353
+ font-family: "DM Sans", system-ui, sans-serif;
354
+ }
355
+ html, body, #root {
356
+ min-height: 100%;
357
+ margin: 0;
358
+ background: var(--bg-primary);
359
+ color: var(--text-primary);
360
+ }
361
+ #root { position: relative; z-index: 1; }
362
+ body::before {
363
+ content: "";
364
+ position: fixed;
365
+ top: -200px;
366
+ left: 50%;
367
+ transform: translateX(-50%);
368
+ width: 900px;
369
+ height: 500px;
370
+ background: radial-gradient(ellipse at center, var(--accent-subtle) 0%, transparent 70%);
371
+ pointer-events: none;
372
+ z-index: 0;
373
+ }
374
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
375
+ ::-webkit-scrollbar-track { background: transparent; }
376
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
377
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-hover); }
378
+ @keyframes fade-up {
379
+ from { opacity: 0; transform: translateY(8px); }
380
+ to { opacity: 1; transform: translateY(0); }
381
+ }
382
+ @keyframes pulse-dot {
383
+ 0%, 100% { opacity: 0.3; }
384
+ 50% { opacity: 1; }
385
+ }
386
+ @keyframes shimmer {
387
+ 0% { background-position: -200% 0; }
388
+ 100% { background-position: 200% 0; }
389
+ }
390
+ perspective-viewer, perspective-viewer * {
391
+ transition: none !important;
392
+ }
393
+ perspective-viewer {
394
+ width: 100%;
395
+ height: 100%;
396
+ --plugin--font-family: "Inter", system-ui, sans-serif;
397
+ }
398
+ </style>
399
+ <script id="definite-app-manifest" type="application/json">${escapeInlineScript(JSON.stringify(manifest))}</script>
400
+ ${previewData ? `<script id="definite-app-preview-data" type="application/json">${escapeInlineScript(JSON.stringify(previewData))}</script>` : ""}
401
+ </head>
402
+ <body>
403
+ <div id="root"></div>
404
+ <script type="module">${escapeInlineScript(jsFile.text)}</script>
405
+ </body>
406
+ </html>
407
+ `;
408
+
409
+ await writeFile(path.join(outDir, "index.html"), html, "utf8");
410
+ console.log(`Built ${path.join(outDir, "index.html")}`);
411
+
412
+ // =============================================================================
413
+ // Embedded variant: strip query declarations from the manifest and inject a
414
+ // placeholder script tag that the Definite backend swaps for the real token
415
+ // at serve time when the app is fetched via the embed route.
416
+ // =============================================================================
417
+
418
+ function buildEmbeddedManifest(src) {
419
+ const strippedResources = {};
420
+ for (const [key, resource] of Object.entries(src.resources ?? {})) {
421
+ if (!resource || typeof resource !== "object") {
422
+ strippedResources[key] = resource;
423
+ continue;
424
+ }
425
+ const source = resource.source ?? null;
426
+ if (source && typeof source === "object" && (source.type === "sql" || source.type === "cube")) {
427
+ // Replace the source with just its type + embedded:true so the runtime
428
+ // knows which backend branch to call, but doesn't see the raw SQL or
429
+ // cube query JSON.
430
+ strippedResources[key] = {
431
+ ...resource,
432
+ source: { type: source.type, embedded: true },
433
+ };
434
+ } else {
435
+ strippedResources[key] = resource;
436
+ }
437
+ }
438
+ return { ...src, resources: strippedResources };
439
+ }
440
+
441
+ const embeddedManifest = buildEmbeddedManifest(manifest);
442
+ const EMBED_TOKEN_PLACEHOLDER =
443
+ '<script id="__definite_embed_token">window.__DEFINITE_EMBED=null;</script>';
444
+
445
+ const embeddedHtml = html
446
+ .replace(
447
+ `<script id="definite-app-manifest" type="application/json">${escapeInlineScript(JSON.stringify(manifest))}</script>`,
448
+ `<script id="definite-app-manifest" type="application/json">${escapeInlineScript(JSON.stringify(embeddedManifest))}</script>\n ${EMBED_TOKEN_PLACEHOLDER}`,
449
+ );
450
+
451
+ if (!embeddedHtml.includes(EMBED_TOKEN_PLACEHOLDER)) {
452
+ throw new Error(
453
+ "build.mjs failed to emit the __definite_embed_token placeholder in the embedded HTML. " +
454
+ "The placeholder script tag format must match what the Definite embed route expects.",
455
+ );
456
+ }
457
+
458
+ await writeFile(path.join(outDir, "index.embedded.html"), embeddedHtml, "utf8");
459
+ console.log(`Built ${path.join(outDir, "index.embedded.html")}`);
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 2,
3
+ "name": "My App",
4
+ "entry": "src/main.tsx",
5
+ "resources": {
6
+ "main": {
7
+ "kind": "dataset",
8
+ "source": {
9
+ "type": "sql",
10
+ "sql": "SELECT id, originated, name FROM LAKE.SCHEMA.sample_events LIMIT 10000"
11
+ },
12
+ "public": false
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env -S uv run
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = ["pyarrow"]
5
+ # ///
6
+ """Generate a tiny synthetic dataset for _refined_demo.
7
+
8
+ Writes data/sample.parquet (100 rows) so CI (and local previews) can verify
9
+ the refined template + runtime actually start up against real preview data.
10
+
11
+ Schema matches what examples/_refined_demo/src/App.tsx queries:
12
+ - id (int) row identifier
13
+ - originated (string) ISO date used as DATE_COLUMN in App.tsx
14
+ - name (string) free-form label for detail views
15
+
16
+ Dates are spread from 2020-01 through 2029-12 so the default "Last 12 months"
17
+ date filter has rows to pick up regardless of when the test runs.
18
+
19
+ Run: uv run examples/_refined_demo/gen_preview_data.py
20
+ """
21
+
22
+ import random
23
+ from datetime import date, timedelta
24
+ from pathlib import Path
25
+
26
+ import pyarrow as pa
27
+ import pyarrow.parquet as pq
28
+
29
+ HERE = Path(__file__).parent
30
+ DATA_DIR = HERE / "data"
31
+ DATA_DIR.mkdir(exist_ok=True)
32
+ PARQUET_PATH = DATA_DIR / "sample.parquet"
33
+
34
+ random.seed(42)
35
+ START = date(2020, 1, 1)
36
+ END = date(2029, 12, 31)
37
+ SPAN_DAYS = (END - START).days
38
+
39
+ NAMES = [
40
+ "Acme",
41
+ "Globex",
42
+ "Initech",
43
+ "Umbrella",
44
+ "Soylent",
45
+ "Hooli",
46
+ "Vandelay",
47
+ "Pied Piper",
48
+ "Stark",
49
+ "Wayne",
50
+ ]
51
+
52
+ ROWS = 100
53
+ ids = list(range(1, ROWS + 1))
54
+ originated = [(START + timedelta(days=random.randint(0, SPAN_DAYS))).isoformat() for _ in range(ROWS)]
55
+ names = [random.choice(NAMES) for _ in range(ROWS)]
56
+
57
+ table = pa.table({"id": ids, "originated": originated, "name": names})
58
+ pq.write_table(table, PARQUET_PATH)
59
+ print(f"Wrote {PARQUET_PATH} ({ROWS} rows)")
@@ -0,0 +1,13 @@
1
+ {
2
+ "context": {
3
+ "publicMode": false,
4
+ "driveFile": "preview://data-apps/_refined_demo",
5
+ "appVersion": "v2"
6
+ },
7
+ "datasets": {
8
+ "main": {
9
+ "file": "data/sample.parquet",
10
+ "format": "parquet"
11
+ }
12
+ }
13
+ }