framework-py 0.1.0.dev0__py3-none-any.whl

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 (63) hide show
  1. framework/__init__.py +250 -0
  2. framework/__main__.py +89 -0
  3. framework/_assets/hooks/pre-commit +59 -0
  4. framework/_assets/init.sql +63 -0
  5. framework/_assets/schema/layer.v1.json +605 -0
  6. framework/_assets/schema/layers.v1.json +791 -0
  7. framework/_assets/schema/spec.v1.json +604 -0
  8. framework/_log.py +29 -0
  9. framework/_paths.py +45 -0
  10. framework/_secretservice.py +69 -0
  11. framework/_time.py +11 -0
  12. framework/_wincred.py +105 -0
  13. framework/_yaml.py +90 -0
  14. framework/ai.py +204 -0
  15. framework/bootstrap.py +206 -0
  16. framework/cache.py +335 -0
  17. framework/catalog.py +193 -0
  18. framework/cloud.py +899 -0
  19. framework/config_home.py +194 -0
  20. framework/connections.py +442 -0
  21. framework/console.py +41 -0
  22. framework/create.py +579 -0
  23. framework/data.py +519 -0
  24. framework/db.py +222 -0
  25. framework/dotenv.py +79 -0
  26. framework/errors.py +39 -0
  27. framework/global_config.py +238 -0
  28. framework/hooks.py +364 -0
  29. framework/integrity.py +22 -0
  30. framework/notebooks.py +384 -0
  31. framework/outputs.py +324 -0
  32. framework/packages.py +276 -0
  33. framework/project.py +52 -0
  34. framework/py.typed +0 -0
  35. framework/quarto.py +163 -0
  36. framework/scaffold.py +269 -0
  37. framework/scratch.py +108 -0
  38. framework/security.py +646 -0
  39. framework/settings.py +439 -0
  40. framework/spec/__init__.py +0 -0
  41. framework/spec/check.py +211 -0
  42. framework/spec/merge.py +195 -0
  43. framework/spec/resolve.py +254 -0
  44. framework/storage.py +516 -0
  45. framework/templates/defaults.yml +144 -0
  46. framework/templates/gitignore/course +120 -0
  47. framework/templates/gitignore/presentation +114 -0
  48. framework/templates/gitignore/project +108 -0
  49. framework/templates/gitignore/sensitive +139 -0
  50. framework/templates/skills/framework-data/SKILL.md +47 -0
  51. framework/templates/skills/framework-outputs/SKILL.md +68 -0
  52. framework/templates/skills/framework-packages/SKILL.md +33 -0
  53. framework/templates/skills/framework-sensitive-data/SKILL.md +38 -0
  54. framework/templates/skills/framework-workflow/SKILL.md +58 -0
  55. framework/templates/stubs/notebook-default.ipynb +34 -0
  56. framework/templates/stubs/notebook-default.qmd +43 -0
  57. framework/templates/stubs/notebook-minimal.qmd +15 -0
  58. framework/templates/stubs/notebook-revealjs.qmd +34 -0
  59. framework/templates/stubs/script-default.py +18 -0
  60. framework_py-0.1.0.dev0.dist-info/METADATA +111 -0
  61. framework_py-0.1.0.dev0.dist-info/RECORD +63 -0
  62. framework_py-0.1.0.dev0.dist-info/WHEEL +4 -0
  63. framework_py-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
framework/storage.py ADDED
@@ -0,0 +1,516 @@
1
+ """S3-compatible storage buckets and publishing.
2
+
3
+ Buckets live under ``connections.storage_buckets`` in the settings::
4
+
5
+ connections:
6
+ default_storage_bucket: reports
7
+ storage_buckets:
8
+ reports: {bucket: my-reports, region: us-east-1, prefix: project-x}
9
+
10
+ Credentials come from the environment (``S3_ACCESS_KEY`` and
11
+ ``S3_SECRET_KEY``, or the AWS variables), usually set in ``.env``. As in R,
12
+ environment variables take precedence over values in the settings.
13
+
14
+ Publishing uploads files with a public-read ACL and returns their URLs.
15
+ Sources inside a ``private`` folder of the project are refused unless
16
+ ``allow_private=True`` is passed, as in R.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import gzip
22
+ import mimetypes
23
+ import os
24
+ import re
25
+ import shutil
26
+ import subprocess
27
+ import tempfile
28
+ from collections.abc import Mapping
29
+ from dataclasses import dataclass
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ import pandas as pd
34
+
35
+ from framework._log import inform
36
+ from framework.errors import FrameworkError
37
+ from framework.settings import settings_read
38
+
39
+ _CONTENT_TYPES = {
40
+ ".html": "text/html",
41
+ ".htm": "text/html",
42
+ ".css": "text/css",
43
+ ".js": "application/javascript",
44
+ ".json": "application/json",
45
+ ".xml": "application/xml",
46
+ ".csv": "text/csv",
47
+ ".txt": "text/plain",
48
+ ".md": "text/markdown",
49
+ ".png": "image/png",
50
+ ".jpg": "image/jpeg",
51
+ ".jpeg": "image/jpeg",
52
+ ".gif": "image/gif",
53
+ ".svg": "image/svg+xml",
54
+ ".pdf": "application/pdf",
55
+ ".zip": "application/zip",
56
+ ".gz": "application/gzip",
57
+ ".parquet": "application/vnd.apache.parquet",
58
+ }
59
+
60
+
61
+ def _section(config: Mapping[str, Any], key: str) -> dict[str, Any]:
62
+ value = config.get(key)
63
+ return value if isinstance(value, dict) else {}
64
+
65
+
66
+ def bucket_configs(config: Mapping[str, Any] | None = None) -> dict[str, dict[str, Any]]:
67
+ """Every storage bucket in the settings, by name.
68
+
69
+ Read from ``connections.storage_buckets``, and from a top-level
70
+ ``storage_buckets`` section, where projects created by R before 2.0 have it.
71
+ """
72
+ config = config if config is not None else settings_read()
73
+ found: dict[str, dict[str, Any]] = {}
74
+ for source in (
75
+ _section(config, "storage_buckets"),
76
+ _section(_section(config, "connections"), "storage_buckets"),
77
+ ):
78
+ found.update(
79
+ {name: dict(entry) for name, entry in source.items() if isinstance(entry, dict)}
80
+ )
81
+ return found
82
+
83
+
84
+ def default_bucket(config: Mapping[str, Any] | None = None) -> str | None:
85
+ config = config if config is not None else settings_read()
86
+ value = _section(config, "connections").get("default_storage_bucket") or config.get(
87
+ "default_storage_bucket"
88
+ )
89
+ return str(value) if value else None
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class Bucket:
94
+ """A resolved storage bucket: where uploads go and how to reach it."""
95
+
96
+ name: str
97
+ bucket: str
98
+ region: str
99
+ prefix: str = ""
100
+ endpoint: str | None = None
101
+ use_https: bool = True
102
+ static_hosting: bool = False
103
+ access_key: str = ""
104
+ secret_key: str = ""
105
+ session_token: str | None = None
106
+
107
+ def key(self, dest: str) -> str:
108
+ key = f"{self.prefix}/{dest}" if self.prefix else dest
109
+ return re.sub(r"/+", "/", key).lstrip("/")
110
+
111
+ def url(self, key: str) -> str:
112
+ """The public URL of an object."""
113
+ if self.endpoint:
114
+ protocol = "https" if self.use_https else "http"
115
+ return f"{protocol}://{self.endpoint}/{self.bucket}/{key}"
116
+ return f"https://{self.bucket}.s3.{self.region}.amazonaws.com/{key}"
117
+
118
+ def client(self) -> Any:
119
+ try:
120
+ import boto3
121
+ except ImportError as error:
122
+ raise FrameworkError(
123
+ "Storage buckets need the boto3 package. "
124
+ "Install it with: pip install framework-py[s3]"
125
+ ) from error
126
+ endpoint = (
127
+ f"{'https' if self.use_https else 'http'}://{self.endpoint}" if self.endpoint else None
128
+ )
129
+ return boto3.client(
130
+ "s3",
131
+ aws_access_key_id=self.access_key,
132
+ aws_secret_access_key=self.secret_key,
133
+ aws_session_token=self.session_token,
134
+ region_name=self.region,
135
+ endpoint_url=endpoint,
136
+ )
137
+
138
+
139
+ def _first(
140
+ config: Mapping[str, Any], keys: tuple[str, ...], env: tuple[str, ...], default: str = ""
141
+ ) -> str:
142
+ for name in env:
143
+ if os.environ.get(name):
144
+ return os.environ[name]
145
+ for key in keys:
146
+ if config.get(key):
147
+ return str(config[key])
148
+ return default
149
+
150
+
151
+ def _build(name: str, entry: Mapping[str, Any]) -> Bucket:
152
+ access = _first(entry, ("access_key_id", "access_key"), ("S3_ACCESS_KEY", "AWS_ACCESS_KEY_ID"))
153
+ secret = _first(
154
+ entry, ("secret_access_key", "secret_key"), ("S3_SECRET_KEY", "AWS_SECRET_ACCESS_KEY")
155
+ )
156
+ if not access or not secret:
157
+ raise FrameworkError(
158
+ "S3 credentials not found. Provide them in the connection settings or set\n"
159
+ " S3_ACCESS_KEY and S3_SECRET_KEY (or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)"
160
+ )
161
+ bucket = _first(entry, ("bucket",), ("S3_BUCKET",))
162
+ if not bucket:
163
+ raise FrameworkError(
164
+ "S3 bucket not specified. Set it in the connection settings or S3_BUCKET"
165
+ )
166
+ endpoint = _first(entry, ("endpoint",), ("S3_ENDPOINT",))
167
+ return Bucket(
168
+ name=name,
169
+ bucket=bucket,
170
+ region=_first(
171
+ entry, ("region",), ("S3_REGION", "AWS_REGION", "AWS_DEFAULT_REGION"), "us-east-1"
172
+ ),
173
+ prefix=str(entry.get("prefix") or ""),
174
+ endpoint=re.sub(r"^https?://", "", endpoint).rstrip("/") or None,
175
+ use_https=not endpoint.startswith("http://"),
176
+ static_hosting=entry.get("static_hosting") is True,
177
+ access_key=access,
178
+ secret_key=secret,
179
+ session_token=_first(entry, ("session_token",), ("S3_SESSION_TOKEN", "AWS_SESSION_TOKEN"))
180
+ or None,
181
+ )
182
+
183
+
184
+ def resolve_bucket(connection: str | None = None) -> Bucket:
185
+ """The named bucket, else the default, else one marked ``default: true``, else the first."""
186
+ config = settings_read()
187
+ buckets = bucket_configs(config)
188
+ if not buckets:
189
+ raise FrameworkError(
190
+ "No storage buckets configured. Add one in settings/connections.yml:\n\n"
191
+ "connections:\n storage_buckets:\n my_s3:\n bucket: my-bucket\n"
192
+ " region: us-east-1\n default_storage_bucket: my_s3"
193
+ )
194
+ if connection is not None:
195
+ if connection not in buckets:
196
+ raise FrameworkError(
197
+ f"Storage bucket '{connection}' not found. Available: {', '.join(buckets)}"
198
+ )
199
+ return _build(connection, buckets[connection])
200
+ name = default_bucket(config)
201
+ if name not in buckets:
202
+ name = next(
203
+ (n for n, e in buckets.items() if e.get("default") is True), next(iter(buckets))
204
+ )
205
+ return _build(name, buckets[name])
206
+
207
+
208
+ # Uploading --------------------------------------------------------------------------
209
+
210
+
211
+ def _refuse_private(path: Path, allow_private: bool) -> None:
212
+ """Refuse a source inside a folder named ``private`` within the project.
213
+
214
+ Folders above the project root do not count (macOS keeps temporary files
215
+ under ``/private``), and nothing is refused outside a project.
216
+ """
217
+ from framework.project import find_settings_file
218
+
219
+ settings_file = find_settings_file()
220
+ if allow_private or settings_file is None:
221
+ return
222
+ root = settings_file.parent.resolve()
223
+ resolved = path.resolve()
224
+ if not resolved.is_relative_to(root):
225
+ return
226
+ relative = resolved.relative_to(root)
227
+ if "private" in relative.parts:
228
+ raise FrameworkError(
229
+ f"Refusing to publish '{relative.as_posix()}': it is inside a private folder, which "
230
+ "holds restricted data. Pass allow_private=True if you really mean to publish it."
231
+ )
232
+
233
+
234
+ def _content_type(path: Path) -> str:
235
+ return (
236
+ _CONTENT_TYPES.get(path.suffix.lower())
237
+ or mimetypes.guess_type(path.name)[0]
238
+ or ("application/octet-stream")
239
+ )
240
+
241
+
242
+ def upload_file(path: Path, dest: str, bucket: Bucket) -> str:
243
+ """Upload one file with a public-read ACL and return its URL."""
244
+ if not path.is_file():
245
+ raise FrameworkError(f"File not found: {path}")
246
+ key = bucket.key(dest)
247
+ client = bucket.client()
248
+ extra = {"ContentType": _content_type(path), "ACL": "public-read"}
249
+ try:
250
+ client.upload_file(str(path), bucket.bucket, key, ExtraArgs=extra)
251
+ except Exception as error:
252
+ # Buckets with ACLs turned off reject the ACL; their policy decides access.
253
+ if "AccessControlListNotSupported" not in str(error):
254
+ raise FrameworkError(f"Failed to upload to S3: {error}") from error
255
+ del extra["ACL"]
256
+ try:
257
+ client.upload_file(str(path), bucket.bucket, key, ExtraArgs=extra)
258
+ except Exception as retry_error:
259
+ raise FrameworkError(f"Failed to upload to S3: {retry_error}") from retry_error
260
+ return bucket.url(key)
261
+
262
+
263
+ def _files(folder: Path, recursive: bool, pattern: str | None) -> list[Path]:
264
+ found = folder.rglob("*") if recursive else folder.iterdir()
265
+ files = sorted(p for p in found if p.is_file())
266
+ if pattern is not None:
267
+ files = [p for p in files if re.search(pattern, p.as_posix())]
268
+ return files
269
+
270
+
271
+ def _upload_folder(folder: Path, dest: str, bucket: Bucket, files: list[Path]) -> list[str]:
272
+ return [upload_file(p, f"{dest}/{p.relative_to(folder).as_posix()}", bucket) for p in files]
273
+
274
+
275
+ def _has_project_token() -> bool:
276
+ from framework.dotenv import value
277
+ from framework.project import find_settings_file
278
+
279
+ if os.environ.get("FW_PROJECT_TOKEN"):
280
+ return True
281
+ settings_file = find_settings_file()
282
+ return bool(settings_file and value(settings_file.parent / ".env", "FW_PROJECT_TOKEN"))
283
+
284
+
285
+ def _target(connection: str | None) -> str:
286
+ if connection is not None or bucket_configs():
287
+ return "s3"
288
+ if _has_project_token():
289
+ return "cloud"
290
+ raise FrameworkError(
291
+ "Nowhere to publish. Add a storage bucket in settings/connections.yml, or set up this "
292
+ "project from Framework Cloud.\nA storage bucket looks like this:\n"
293
+ "connections:\n storage_buckets:\n my_s3:\n bucket: my-bucket\n"
294
+ " region: us-east-1\n default_storage_bucket: my_s3"
295
+ )
296
+
297
+
298
+ def _slug_ignored(slug: str | bool | None) -> None:
299
+ if slug is not None:
300
+ import warnings
301
+
302
+ from framework.errors import FrameworkWarning
303
+
304
+ warnings.warn(
305
+ "`slug` applies to Framework Cloud only; ignored for storage buckets.",
306
+ FrameworkWarning,
307
+ stacklevel=3,
308
+ )
309
+
310
+
311
+ def publish(
312
+ source: str | Path,
313
+ dest: str | None = None,
314
+ connection: str | None = None,
315
+ overwrite: bool = True,
316
+ self_contained: bool = True,
317
+ slug: str | bool | None = None,
318
+ allow_private: bool = False,
319
+ ) -> str | list[str]:
320
+ """Upload a file or folder to the project's storage bucket.
321
+
322
+ Args:
323
+ source: A file or folder.
324
+ dest: Where to put it in the bucket. Default: the source's name.
325
+ connection: A storage bucket name. Default: the project's default bucket.
326
+ overwrite: Accepted for parity with R; uploads always replace.
327
+ self_contained: For Framework Cloud publishing.
328
+ slug: For Framework Cloud publishing.
329
+ allow_private: Publish even though the source is inside a ``private``
330
+ folder. Only a person who has reviewed the file should set this.
331
+
332
+ Returns:
333
+ The public URL, or a list of URLs for a folder.
334
+ """
335
+ path = Path(source)
336
+ if not path.exists():
337
+ raise FrameworkError(f"Source not found: {path}")
338
+ _refuse_private(path, allow_private)
339
+ if _target(connection) == "cloud":
340
+ from framework.cloud import cloud_publish
341
+
342
+ return cloud_publish(path, dest=dest, self_contained=self_contained, slug=slug)
343
+ _slug_ignored(slug)
344
+ bucket = resolve_bucket(connection)
345
+ dest = dest or path.name
346
+ if path.is_dir():
347
+ urls = _upload_folder(path, dest, bucket, _files(path, True, None))
348
+ inform(f"Published {len(urls)} files to S3")
349
+ return urls
350
+ url = upload_file(path, dest, bucket)
351
+ inform(f"Published: {url}")
352
+ return url
353
+
354
+
355
+ def publish_notebook(
356
+ file: str | Path,
357
+ dest: str | None = None,
358
+ connection: str | None = None,
359
+ self_contained: bool = True,
360
+ format: str = "html",
361
+ slug: str | bool | None = None,
362
+ allow_private: bool = False,
363
+ ) -> str:
364
+ """Render a Quarto notebook with the ``quarto`` command and upload the result.
365
+
366
+ A bucket with ``static_hosting: true`` gets ``<dest>/index.html`` and a
367
+ clean URL ending in ``/``. Other buckets get ``<dest>.html`` as one
368
+ self-contained file.
369
+ """
370
+ source = Path(file)
371
+ if not source.is_file() or source.suffix.lower() not in (".qmd", ".ipynb"):
372
+ raise FrameworkError(f"publish_notebook() needs an existing .qmd or .ipynb file: {source}")
373
+ _refuse_private(source, allow_private)
374
+ quarto = shutil.which("quarto")
375
+ if quarto is None:
376
+ raise FrameworkError(
377
+ "Quarto not found. Install it from https://quarto.org/docs/get-started/"
378
+ )
379
+ if _target(connection) == "cloud":
380
+ if format != "html":
381
+ raise FrameworkError(f'Framework Cloud publishes HTML only (got format="{format}").')
382
+ from framework.cloud import cloud_publish
383
+
384
+ return cloud_publish(source, dest=dest, self_contained=self_contained, slug=slug)
385
+ _slug_ignored(slug)
386
+ bucket = resolve_bucket(connection)
387
+ self_contained = self_contained or not bucket.static_hosting
388
+
389
+ with tempfile.TemporaryDirectory(prefix="publish_notebook_") as temp:
390
+ command = [quarto, "render", str(source.resolve()), "--output-dir", temp, "--to", format]
391
+ if self_contained:
392
+ command.append("--embed-resources")
393
+ inform(f"Rendering {source.name}...")
394
+ result = subprocess.run(command, capture_output=True, text=True, check=False)
395
+ if result.returncode != 0:
396
+ raise FrameworkError(f"Quarto render failed:\n{result.stdout}{result.stderr}")
397
+ outputs = sorted(Path(temp).glob("*.html"))
398
+ if not outputs:
399
+ raise FrameworkError("No HTML output found after rendering")
400
+ base = re.sub(r"\.html$", "", (dest or source.stem).rstrip("/"))
401
+
402
+ if bucket.static_hosting:
403
+ if self_contained:
404
+ upload_file(outputs[0], f"{base}/index.html", bucket)
405
+ else:
406
+ index = Path(temp) / "index.html"
407
+ if outputs[0] != index:
408
+ outputs[0].rename(index)
409
+ urls = _upload_folder(Path(temp), base, bucket, _files(Path(temp), True, None))
410
+ inform(f"Published {len(urls)} files")
411
+ url = bucket.url(bucket.key(f"{base}/"))
412
+ else:
413
+ url = upload_file(outputs[0], f"{base}.html", bucket)
414
+ inform(f"Published: {url}")
415
+ return url
416
+
417
+
418
+ def publish_data(
419
+ data: pd.DataFrame | str | Path,
420
+ dest: str,
421
+ format: str = "csv",
422
+ connection: str | None = None,
423
+ compress: bool = False,
424
+ allow_private: bool = False,
425
+ ) -> str:
426
+ """Upload a DataFrame (as CSV, Parquet, or JSON) or a data file.
427
+
428
+ With ``compress``, the file is gzipped and ``.gz`` is added to ``dest``.
429
+ """
430
+ if format not in ("csv", "parquet", "json"):
431
+ raise FrameworkError("format must be csv, parquet, or json (rds is R-only)")
432
+ bucket = resolve_bucket(connection)
433
+ with tempfile.TemporaryDirectory(prefix="publish_data_") as temp:
434
+ if isinstance(data, pd.DataFrame):
435
+ source = Path(temp) / f"data.{format}"
436
+ if format == "csv":
437
+ data.to_csv(source, index=False, lineterminator="\n")
438
+ elif format == "parquet":
439
+ data.to_parquet(source, index=False)
440
+ else:
441
+ data.to_json(source, orient="records")
442
+ else:
443
+ source = Path(data)
444
+ if not source.is_file():
445
+ raise FrameworkError(f"File not found: {source}")
446
+ _refuse_private(source, allow_private)
447
+ if compress:
448
+ compressed = Path(temp) / f"{source.name}.gz"
449
+ with source.open("rb") as raw, gzip.open(compressed, "wb") as packed:
450
+ shutil.copyfileobj(raw, packed)
451
+ source = compressed
452
+ dest = dest if dest.endswith(".gz") else f"{dest}.gz"
453
+ url = upload_file(source, dest, bucket)
454
+ inform(f"Published: {url}")
455
+ return url
456
+
457
+
458
+ def publish_dir(
459
+ dir: str | Path,
460
+ dest: str | None = None,
461
+ connection: str | None = None,
462
+ pattern: str | None = None,
463
+ recursive: bool = True,
464
+ allow_private: bool = False,
465
+ ) -> list[str]:
466
+ """Upload the files in a folder. ``pattern`` is a regular expression on the paths."""
467
+ folder = Path(dir)
468
+ if not folder.is_dir():
469
+ raise FrameworkError(f"Directory not found: {folder}")
470
+ _refuse_private(folder, allow_private)
471
+ bucket = resolve_bucket(connection)
472
+ files = _files(folder, recursive, pattern)
473
+ if not files:
474
+ inform("No files found to upload")
475
+ return []
476
+ dest = dest or folder.resolve().name
477
+ inform(f"Uploading {len(files)} files...")
478
+ urls = _upload_folder(folder, dest, bucket, files)
479
+ inform(f"Published {len(urls)} files to {dest}")
480
+ return urls
481
+
482
+
483
+ def publish_list(
484
+ prefix: str | None = None, connection: str | None = None, max: int = 1000
485
+ ) -> pd.DataFrame:
486
+ """Objects in the bucket (under its prefix): key, size, last_modified, and url."""
487
+ bucket = resolve_bucket(connection)
488
+ full_prefix = bucket.key(prefix) if prefix else bucket.prefix
489
+ try:
490
+ response = bucket.client().list_objects_v2(
491
+ Bucket=bucket.bucket, Prefix=full_prefix, MaxKeys=max
492
+ )
493
+ except Exception as error:
494
+ raise FrameworkError(f"Failed to list S3 objects: {error}") from error
495
+ rows = [
496
+ {
497
+ "key": item["Key"],
498
+ "size": item.get("Size"),
499
+ "last_modified": str(item.get("LastModified", "")),
500
+ "url": bucket.url(item["Key"]),
501
+ }
502
+ for item in response.get("Contents", [])
503
+ ]
504
+ return pd.DataFrame(rows, columns=["key", "size", "last_modified", "url"])
505
+
506
+
507
+ def storage_test(connection: str | None = None) -> bool:
508
+ """Whether the bucket exists and the credentials can reach it."""
509
+ bucket = resolve_bucket(connection)
510
+ try:
511
+ bucket.client().head_bucket(Bucket=bucket.bucket)
512
+ except Exception as error:
513
+ inform(f"S3 connection failed: {error}")
514
+ return False
515
+ inform(f"S3 connection OK: {bucket.name} (bucket: {bucket.bucket})")
516
+ return True
@@ -0,0 +1,144 @@
1
+ # Package defaults for Python projects: layer 0 of spec_resolve().
2
+ # Directory layouts and render folders match the R package, so a project's
3
+ # data and output folders are the same whichever language created it.
4
+
5
+ author: {name: "", email: "", affiliation: ""}
6
+
7
+ packages:
8
+ use_renv: false
9
+ default_packages: {}
10
+
11
+ git:
12
+ use_git: true
13
+ hooks: {data_security: false}
14
+
15
+ ai:
16
+ enabled: true
17
+ assistants: {claude: true}
18
+ agents_md: stub
19
+
20
+ scaffold:
21
+ seed_on_scaffold: false
22
+ seed: "123"
23
+ source_all_functions: true
24
+ notebook_format: quarto
25
+ ide: vscode
26
+
27
+ quarto:
28
+ html:
29
+ format: html
30
+ embed_resources: true
31
+ theme: default
32
+ toc: true
33
+ toc_depth: 3
34
+ code_fold: false
35
+ code_tools: false
36
+ highlight_style: github
37
+ revealjs:
38
+ format: revealjs
39
+ theme: default
40
+ incremental: false
41
+ slide_number: true
42
+ transition: slide
43
+ background_transition: fade
44
+ controls: true
45
+ progress: true
46
+ center: true
47
+ highlight_style: github
48
+
49
+ connections:
50
+ default_database: framework
51
+ default_storage_bucket: null
52
+ databases:
53
+ framework: {driver: sqlite, database: framework.db}
54
+ storage_buckets: {}
55
+
56
+ env:
57
+ POSTGRES_HOST: ""
58
+ POSTGRES_PORT: "5432"
59
+ POSTGRES_DB: ""
60
+ POSTGRES_SCHEMA: public
61
+ POSTGRES_USER: ""
62
+ POSTGRES_PASSWORD: ""
63
+ S3_BUCKET: ""
64
+ S3_ACCESS_KEY: ""
65
+ S3_SECRET_KEY: ""
66
+ S3_REGION: ""
67
+ S3_ENDPOINT: ""
68
+ S3_SESSION_TOKEN: ""
69
+
70
+ skills: [framework-workflow, framework-data, framework-packages, framework-outputs]
71
+ sensitive_skills: [framework-sensitive-data]
72
+
73
+ types:
74
+ project:
75
+ directories:
76
+ notebooks: notebooks
77
+ scripts: scripts
78
+ functions: functions
79
+ inputs_raw: inputs/raw
80
+ inputs_intermediate: inputs/intermediate
81
+ inputs_final: inputs/final
82
+ docs: docs
83
+ outputs_notebooks: outputs/notebooks
84
+ outputs_tables: outputs/tables
85
+ outputs_figures: outputs/figures
86
+ outputs_models: outputs/models
87
+ outputs_reports: outputs/reports
88
+ cache: outputs/cache
89
+ scratch: scratch
90
+ logs: logs
91
+ render_dirs: {notebooks: outputs/notebooks, docs: outputs/docs}
92
+ render_dir: outputs/notebooks
93
+ project_sensitive:
94
+ directories:
95
+ functions: functions
96
+ notebooks: notebooks
97
+ scripts: scripts
98
+ inputs_private_raw: inputs/private/raw
99
+ inputs_public_raw: inputs/public/raw
100
+ inputs_private_intermediate: inputs/private/intermediate
101
+ inputs_public_intermediate: inputs/public/intermediate
102
+ inputs_private_final: inputs/private/final
103
+ inputs_public_final: inputs/public/final
104
+ outputs_private_notebooks: outputs/private/notebooks
105
+ outputs_public_notebooks: outputs/public/notebooks
106
+ outputs_private_tables: outputs/private/tables
107
+ outputs_public_tables: outputs/public/tables
108
+ outputs_private_figures: outputs/private/figures
109
+ outputs_public_figures: outputs/public/figures
110
+ outputs_private_models: outputs/private/models
111
+ outputs_public_models: outputs/public/models
112
+ outputs_private_reports: outputs/private/reports
113
+ outputs_public_reports: outputs/public/reports
114
+ cache: outputs/private/cache
115
+ scratch: scratch
116
+ logs: logs
117
+ render_dirs: {notebooks: outputs/private/notebooks, docs: outputs/private/docs}
118
+ render_dir: outputs/public/docs
119
+ course:
120
+ directories:
121
+ data: data
122
+ slides: slides
123
+ assignments: assignments
124
+ course_docs: course_docs
125
+ readings: readings
126
+ notebooks: modules
127
+ cache: cache
128
+ render_dirs:
129
+ slides: rendered/slides
130
+ assignments: rendered/assignments
131
+ course_docs: rendered/course_docs
132
+ modules: rendered/modules
133
+ render_dir: rendered/slides
134
+ presentation:
135
+ directories:
136
+ presentation_source: presentation.qmd
137
+ rendered_slides: "."
138
+ data: data
139
+ cache: cache
140
+ render_dirs: {}
141
+ render_dir: "."
142
+ bare:
143
+ directories: {}
144
+ render_dirs: {}