pyfabric 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyfabric-0.1.0/.codespellrc +12 -0
- pyfabric-0.1.0/.gitattributes +18 -0
- pyfabric-0.1.0/.github/CODEOWNERS +2 -0
- pyfabric-0.1.0/.github/dependabot.yml +22 -0
- pyfabric-0.1.0/.github/workflows/ci.yml +70 -0
- pyfabric-0.1.0/.github/workflows/dependency-review.yml +17 -0
- pyfabric-0.1.0/.github/workflows/publish.yml +59 -0
- pyfabric-0.1.0/.gitignore +215 -0
- pyfabric-0.1.0/.markdownlint-cli2.jsonc +25 -0
- pyfabric-0.1.0/.pre-commit-config.yaml +42 -0
- pyfabric-0.1.0/CLAUDE.md +178 -0
- pyfabric-0.1.0/CONTRIBUTING.md +120 -0
- pyfabric-0.1.0/LICENSE +21 -0
- pyfabric-0.1.0/PKG-INFO +258 -0
- pyfabric-0.1.0/README.md +193 -0
- pyfabric-0.1.0/docs/api.md +359 -0
- pyfabric-0.1.0/docs/prompts.md +203 -0
- pyfabric-0.1.0/docs/roadmap.md +57 -0
- pyfabric-0.1.0/docs/testing.md +215 -0
- pyfabric-0.1.0/docs/vision.md +47 -0
- pyfabric-0.1.0/examples/workspace_demo.py +23 -0
- pyfabric-0.1.0/pyproject.toml +211 -0
- pyfabric-0.1.0/src/pyfabric/__init__.py +10 -0
- pyfabric-0.1.0/src/pyfabric/_version.py +24 -0
- pyfabric-0.1.0/src/pyfabric/claude_install.py +336 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/MEMORY.md +8 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/descriptions_required.md +81 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/first_refresh_cred_binding.md +61 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/notebook_required_settings_files.md +63 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/notebook_wheel_resources_pattern.md +179 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/onelake_vs_sql_endpoint.md +46 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/open_mirror_landing_zone.md +210 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/pyfabric.md +86 -0
- pyfabric-0.1.0/src/pyfabric/claude_memory/wheel_sqlconn_pattern.md +86 -0
- pyfabric-0.1.0/src/pyfabric/cli.py +280 -0
- pyfabric-0.1.0/src/pyfabric/client/__init__.py +1 -0
- pyfabric-0.1.0/src/pyfabric/client/auth.py +293 -0
- pyfabric-0.1.0/src/pyfabric/client/graph.py +157 -0
- pyfabric-0.1.0/src/pyfabric/client/http.py +260 -0
- pyfabric-0.1.0/src/pyfabric/client/livy.py +166 -0
- pyfabric-0.1.0/src/pyfabric/client/ontology/__init__.py +135 -0
- pyfabric-0.1.0/src/pyfabric/client/ontology/_id_gen.py +19 -0
- pyfabric-0.1.0/src/pyfabric/client/ontology/builder.py +447 -0
- pyfabric-0.1.0/src/pyfabric/client/ontology/crud.py +70 -0
- pyfabric-0.1.0/src/pyfabric/client/ontology/parts.py +591 -0
- pyfabric-0.1.0/src/pyfabric/client/ontology_sync.py +376 -0
- pyfabric-0.1.0/src/pyfabric/data/__init__.py +17 -0
- pyfabric-0.1.0/src/pyfabric/data/lakehouse.py +549 -0
- pyfabric-0.1.0/src/pyfabric/data/local_lakehouse.py +666 -0
- pyfabric-0.1.0/src/pyfabric/data/onelake.py +495 -0
- pyfabric-0.1.0/src/pyfabric/data/open_mirror.py +526 -0
- pyfabric-0.1.0/src/pyfabric/data/processing_log.py +209 -0
- pyfabric-0.1.0/src/pyfabric/data/schema.py +321 -0
- pyfabric-0.1.0/src/pyfabric/data/sql.py +199 -0
- pyfabric-0.1.0/src/pyfabric/data/sqlconn.py +85 -0
- pyfabric-0.1.0/src/pyfabric/demo.py +288 -0
- pyfabric-0.1.0/src/pyfabric/items/__init__.py +1 -0
- pyfabric-0.1.0/src/pyfabric/items/bundle.py +243 -0
- pyfabric-0.1.0/src/pyfabric/items/crud.py +216 -0
- pyfabric-0.1.0/src/pyfabric/items/environment.py +376 -0
- pyfabric-0.1.0/src/pyfabric/items/mirrored_database.py +342 -0
- pyfabric-0.1.0/src/pyfabric/items/normalize.py +285 -0
- pyfabric-0.1.0/src/pyfabric/items/notebook.py +366 -0
- pyfabric-0.1.0/src/pyfabric/items/report.py +1129 -0
- pyfabric-0.1.0/src/pyfabric/items/semantic_model.py +750 -0
- pyfabric-0.1.0/src/pyfabric/items/types.py +198 -0
- pyfabric-0.1.0/src/pyfabric/items/validate.py +160 -0
- pyfabric-0.1.0/src/pyfabric/items/validate_tmdl.py +116 -0
- pyfabric-0.1.0/src/pyfabric/logging.py +179 -0
- pyfabric-0.1.0/src/pyfabric/py.typed +0 -0
- pyfabric-0.1.0/src/pyfabric/testing/__init__.py +14 -0
- pyfabric-0.1.0/src/pyfabric/testing/analyze.py +76 -0
- pyfabric-0.1.0/src/pyfabric/testing/duckdb_spark.py +268 -0
- pyfabric-0.1.0/src/pyfabric/testing/fixtures.py +252 -0
- pyfabric-0.1.0/src/pyfabric/testing/mock_notebookutils.py +122 -0
- pyfabric-0.1.0/src/pyfabric/testing/plugin.py +9 -0
- pyfabric-0.1.0/src/pyfabric/workspace/__init__.py +1 -0
- pyfabric-0.1.0/src/pyfabric/workspace/workspaces.py +155 -0
- pyfabric-0.1.0/tests/__init__.py +0 -0
- pyfabric-0.1.0/tests/client/__init__.py +0 -0
- pyfabric-0.1.0/tests/client/test_auth.py +151 -0
- pyfabric-0.1.0/tests/client/test_auth_errors.py +101 -0
- pyfabric-0.1.0/tests/client/test_graph_errors.py +84 -0
- pyfabric-0.1.0/tests/client/test_http.py +191 -0
- pyfabric-0.1.0/tests/client/test_http_errors.py +184 -0
- pyfabric-0.1.0/tests/client/test_livy.py +36 -0
- pyfabric-0.1.0/tests/client/test_livy_errors.py +124 -0
- pyfabric-0.1.0/tests/client/test_ontology.py +445 -0
- pyfabric-0.1.0/tests/client/test_ontology_parts.py +374 -0
- pyfabric-0.1.0/tests/conftest.py +217 -0
- pyfabric-0.1.0/tests/data/__init__.py +0 -0
- pyfabric-0.1.0/tests/data/test_lakehouse.py +161 -0
- pyfabric-0.1.0/tests/data/test_lakehouse_ddl.py +308 -0
- pyfabric-0.1.0/tests/data/test_local_lakehouse_push.py +148 -0
- pyfabric-0.1.0/tests/data/test_local_lakehouse_rename_schema.py +91 -0
- pyfabric-0.1.0/tests/data/test_local_lakehouse_schema_drift.py +336 -0
- pyfabric-0.1.0/tests/data/test_onelake.py +32 -0
- pyfabric-0.1.0/tests/data/test_onelake_walk_cache.py +326 -0
- pyfabric-0.1.0/tests/data/test_open_mirror.py +270 -0
- pyfabric-0.1.0/tests/data/test_open_mirror_write_rows.py +425 -0
- pyfabric-0.1.0/tests/data/test_processing_log.py +114 -0
- pyfabric-0.1.0/tests/data/test_sample_db.py +150 -0
- pyfabric-0.1.0/tests/data/test_schema.py +337 -0
- pyfabric-0.1.0/tests/data/test_schema_validation.py +123 -0
- pyfabric-0.1.0/tests/data/test_sql_errors.py +194 -0
- pyfabric-0.1.0/tests/data/test_sqlconn.py +59 -0
- pyfabric-0.1.0/tests/data/test_upload_retry.py +112 -0
- pyfabric-0.1.0/tests/fixtures/create_sample_db.py +164 -0
- pyfabric-0.1.0/tests/fixtures/mirrors/mirror_custom.MirroredDatabase/mirroring.json +15 -0
- pyfabric-0.1.0/tests/fixtures/mirrors/mirror_default.MirroredDatabase/mirroring.json +15 -0
- pyfabric-0.1.0/tests/fixtures/notebooks/nb_full.Notebook/notebook-content.py +52 -0
- pyfabric-0.1.0/tests/fixtures/notebooks/nb_minimal.Notebook/notebook-content.py +9 -0
- pyfabric-0.1.0/tests/fixtures/workspace/df_example.Dataflow/.platform +12 -0
- pyfabric-0.1.0/tests/fixtures/workspace/df_example.Dataflow/mashup.pq +1 -0
- pyfabric-0.1.0/tests/fixtures/workspace/df_example.Dataflow/queryMetadata.json +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/env_example.Environment/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/env_example.Environment/Libraries/PublicLibraries/environment.yml +3 -0
- pyfabric-0.1.0/tests/fixtures/workspace/env_example.Environment/Setting/Sparkcompute.yml +6 -0
- pyfabric-0.1.0/tests/fixtures/workspace/lh_example.Lakehouse/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/lh_example.Lakehouse/lakehouse.metadata.json +1 -0
- pyfabric-0.1.0/tests/fixtures/workspace/nb_example.Notebook/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/nb_example.Notebook/notebook-content.py +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/pl_example.DataPipeline/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/pl_example.DataPipeline/pipeline-content.json +5 -0
- pyfabric-0.1.0/tests/fixtures/workspace/rpt_example.Report/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/rpt_example.Report/report.json +4 -0
- pyfabric-0.1.0/tests/fixtures/workspace/sm_example.SemanticModel/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/sm_example.SemanticModel/model.bim +7 -0
- pyfabric-0.1.0/tests/fixtures/workspace/vl_example.VariableLibrary/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/vl_example.VariableLibrary/settings.json +6 -0
- pyfabric-0.1.0/tests/fixtures/workspace/vl_example.VariableLibrary/valueSets/DEV.json +5 -0
- pyfabric-0.1.0/tests/fixtures/workspace/vl_example.VariableLibrary/variables.json +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace/wh_example.Warehouse/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace_invalid/bad_platform.Notebook/.platform +1 -0
- pyfabric-0.1.0/tests/fixtures/workspace_invalid/bad_platform.Notebook/notebook-content.py +1 -0
- pyfabric-0.1.0/tests/fixtures/workspace_invalid/nb_no_content.Notebook/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace_invalid/wrong_name.Lakehouse/.platform +11 -0
- pyfabric-0.1.0/tests/fixtures/workspace_invalid/wrong_name.Lakehouse/lakehouse.metadata.json +1 -0
- pyfabric-0.1.0/tests/items/__init__.py +0 -0
- pyfabric-0.1.0/tests/items/test_bundle_errors.py +81 -0
- pyfabric-0.1.0/tests/items/test_crud.py +108 -0
- pyfabric-0.1.0/tests/items/test_environment.py +258 -0
- pyfabric-0.1.0/tests/items/test_mirrored_database.py +338 -0
- pyfabric-0.1.0/tests/items/test_normalize.py +191 -0
- pyfabric-0.1.0/tests/items/test_notebook.py +339 -0
- pyfabric-0.1.0/tests/items/test_report.py +785 -0
- pyfabric-0.1.0/tests/items/test_semantic_model.py +605 -0
- pyfabric-0.1.0/tests/items/test_types.py +158 -0
- pyfabric-0.1.0/tests/items/test_validate.py +350 -0
- pyfabric-0.1.0/tests/items/test_validate_e2e.py +73 -0
- pyfabric-0.1.0/tests/items/test_validate_pairwise.py +133 -0
- pyfabric-0.1.0/tests/items/test_validate_tmdl.py +129 -0
- pyfabric-0.1.0/tests/test_claude_install.py +225 -0
- pyfabric-0.1.0/tests/test_cli.py +43 -0
- pyfabric-0.1.0/tests/test_cli_normalize.py +106 -0
- pyfabric-0.1.0/tests/test_demo.py +247 -0
- pyfabric-0.1.0/tests/test_logging.py +120 -0
- pyfabric-0.1.0/tests/test_pyproject_metadata.py +44 -0
- pyfabric-0.1.0/tests/test_version.py +9 -0
- pyfabric-0.1.0/tests/testing/__init__.py +0 -0
- pyfabric-0.1.0/tests/testing/test_analyze.py +15 -0
- pyfabric-0.1.0/tests/testing/test_attach_duckdb_lakehouse.py +134 -0
- pyfabric-0.1.0/tests/testing/test_duckdb_spark.py +157 -0
- pyfabric-0.1.0/tests/testing/test_mock_notebookutils.py +89 -0
- pyfabric-0.1.0/tests/testing/test_snapshot_delta.py +179 -0
- pyfabric-0.1.0/tests/workspace/__init__.py +0 -0
- pyfabric-0.1.0/tests/workspace/test_workspaces.py +56 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[codespell]
|
|
2
|
+
# Skip generated, vendored, and binary content. Notebook fixture files
|
|
3
|
+
# contain raw cell-marker syntax and synthetic IDs that confuse codespell.
|
|
4
|
+
skip = .git,*.pyc,*.pyo,.venv,build,dist,*.egg-info,.test-report.json,tests/fixtures/notebooks/**,src/pyfabric/_version.py
|
|
5
|
+
# Allowlist project-specific words. Add sparingly — every entry here is
|
|
6
|
+
# a future false positive someone has to read past.
|
|
7
|
+
# - falsy: legitimate jargon (truthy/falsy)
|
|
8
|
+
# - caf: matches the literal "café" (café) in test_logging.py's UTF-8
|
|
9
|
+
# round-trip test; codespell sees the ASCII prefix only.
|
|
10
|
+
# - skool: the Skool community platform (https://www.skool.com), used
|
|
11
|
+
# as an example data source in claude_memory/open_mirror_landing_zone.md.
|
|
12
|
+
ignore-words-list = falsy,caf,skool
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Normalize all text files to LF in the repository
|
|
2
|
+
* text=auto eol=lf
|
|
3
|
+
|
|
4
|
+
# Explicitly mark binary files
|
|
5
|
+
*.png binary
|
|
6
|
+
*.jpg binary
|
|
7
|
+
*.whl binary
|
|
8
|
+
|
|
9
|
+
# Ensure key file types use LF everywhere
|
|
10
|
+
*.py text eol=lf
|
|
11
|
+
*.toml text eol=lf
|
|
12
|
+
*.yml text eol=lf
|
|
13
|
+
*.yaml text eol=lf
|
|
14
|
+
*.md text eol=lf
|
|
15
|
+
*.txt text eol=lf
|
|
16
|
+
*.cfg text eol=lf
|
|
17
|
+
*.ini text eol=lf
|
|
18
|
+
*.json text eol=lf
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Keep GitHub Actions current (no CVE tracking for Actions)
|
|
4
|
+
- package-ecosystem: "github-actions"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
groups:
|
|
9
|
+
github-actions:
|
|
10
|
+
patterns:
|
|
11
|
+
- "*"
|
|
12
|
+
|
|
13
|
+
# Python deps: security updates ONLY (CVE-driven)
|
|
14
|
+
# Version updates are handled by Dependabot security updates (repo setting),
|
|
15
|
+
# not dependabot.yml version updates. This entry ensures the dependency
|
|
16
|
+
# graph stays current for vulnerability scanning without auto-bumping
|
|
17
|
+
# versions that have no known CVEs.
|
|
18
|
+
- package-ecosystem: "pip"
|
|
19
|
+
directory: "/"
|
|
20
|
+
schedule:
|
|
21
|
+
interval: "weekly"
|
|
22
|
+
open-pull-requests-limit: 0 # Disable version update PRs
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: Lint & Format
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
- uses: actions/setup-python@v6
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
- run: pip install -e ".[dev]"
|
|
26
|
+
- run: ruff check .
|
|
27
|
+
- run: ruff format --check .
|
|
28
|
+
|
|
29
|
+
docs-lint:
|
|
30
|
+
name: Docs Lint (codespell + markdownlint)
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v6
|
|
34
|
+
- uses: actions/setup-python@v6
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
- run: pip install codespell==2.3.0
|
|
38
|
+
- run: codespell --config=.codespellrc
|
|
39
|
+
- uses: DavidAnson/markdownlint-cli2-action@v17
|
|
40
|
+
|
|
41
|
+
type-check:
|
|
42
|
+
name: Type Check
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v6
|
|
46
|
+
- uses: actions/setup-python@v6
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.12"
|
|
49
|
+
- run: pip install -e ".[dev]"
|
|
50
|
+
- run: mypy src/
|
|
51
|
+
|
|
52
|
+
test:
|
|
53
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
strategy:
|
|
56
|
+
fail-fast: false
|
|
57
|
+
matrix:
|
|
58
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v6
|
|
61
|
+
- uses: actions/setup-python@v6
|
|
62
|
+
with:
|
|
63
|
+
python-version: ${{ matrix.python-version }}
|
|
64
|
+
- run: pip install -e ".[dev]"
|
|
65
|
+
- run: pytest --cov=src/pyfabric --cov-branch --cov-report=term-missing --json-report --json-report-file=test-report.json
|
|
66
|
+
- uses: actions/upload-artifact@v7
|
|
67
|
+
if: always()
|
|
68
|
+
with:
|
|
69
|
+
name: test-report-py${{ matrix.python-version }}
|
|
70
|
+
path: test-report.json
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Dependency Review
|
|
2
|
+
|
|
3
|
+
on: [pull_request]
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
dependency-review:
|
|
10
|
+
name: Dependency Review
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
- uses: actions/dependency-review-action@v4
|
|
15
|
+
with:
|
|
16
|
+
fail-on-severity: high
|
|
17
|
+
deny-licenses: GPL-3.0, AGPL-3.0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0 # Required for hatch-vcs to derive version from tags
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: python -m pip install --upgrade pip
|
|
21
|
+
- run: pip install build pip-audit
|
|
22
|
+
- run: pip-audit
|
|
23
|
+
- run: python -m build
|
|
24
|
+
- uses: anchore/sbom-action@v0
|
|
25
|
+
with:
|
|
26
|
+
path: dist/
|
|
27
|
+
format: spdx-json
|
|
28
|
+
output-file: sbom.spdx.json
|
|
29
|
+
- uses: actions/upload-artifact@v7
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
- uses: actions/upload-artifact@v7
|
|
34
|
+
with:
|
|
35
|
+
name: sbom
|
|
36
|
+
path: sbom.spdx.json
|
|
37
|
+
|
|
38
|
+
publish:
|
|
39
|
+
name: Publish to PyPI
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment:
|
|
43
|
+
name: pypi
|
|
44
|
+
url: https://pypi.org/p/pyfabric
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write
|
|
47
|
+
attestations: write
|
|
48
|
+
contents: read
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/download-artifact@v8
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
- uses: actions/attest-build-provenance@v4
|
|
55
|
+
with:
|
|
56
|
+
subject-path: dist/*
|
|
57
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
58
|
+
with:
|
|
59
|
+
attestations: true
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# hatch-vcs generated version file
|
|
2
|
+
src/pyfabric/_version.py
|
|
3
|
+
|
|
4
|
+
# Test artifacts
|
|
5
|
+
.test-report.json
|
|
6
|
+
.logs/
|
|
7
|
+
tests/fixtures/sample.duckdb
|
|
8
|
+
|
|
9
|
+
# Byte-compiled / optimized / DLL files
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.py[codz]
|
|
12
|
+
*$py.class
|
|
13
|
+
|
|
14
|
+
# C extensions
|
|
15
|
+
*.so
|
|
16
|
+
|
|
17
|
+
# Distribution / packaging
|
|
18
|
+
.Python
|
|
19
|
+
build/
|
|
20
|
+
develop-eggs/
|
|
21
|
+
dist/
|
|
22
|
+
downloads/
|
|
23
|
+
eggs/
|
|
24
|
+
.eggs/
|
|
25
|
+
lib/
|
|
26
|
+
lib64/
|
|
27
|
+
parts/
|
|
28
|
+
sdist/
|
|
29
|
+
var/
|
|
30
|
+
wheels/
|
|
31
|
+
share/python-wheels/
|
|
32
|
+
*.egg-info/
|
|
33
|
+
.installed.cfg
|
|
34
|
+
*.egg
|
|
35
|
+
MANIFEST
|
|
36
|
+
|
|
37
|
+
# PyInstaller
|
|
38
|
+
# Usually these files are written by a python script from a template
|
|
39
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
40
|
+
*.manifest
|
|
41
|
+
*.spec
|
|
42
|
+
|
|
43
|
+
# Installer logs
|
|
44
|
+
pip-log.txt
|
|
45
|
+
pip-delete-this-directory.txt
|
|
46
|
+
|
|
47
|
+
# Unit test / coverage reports
|
|
48
|
+
htmlcov/
|
|
49
|
+
.tox/
|
|
50
|
+
.nox/
|
|
51
|
+
.coverage
|
|
52
|
+
.coverage.*
|
|
53
|
+
.cache
|
|
54
|
+
nosetests.xml
|
|
55
|
+
coverage.xml
|
|
56
|
+
*.cover
|
|
57
|
+
*.py.cover
|
|
58
|
+
.hypothesis/
|
|
59
|
+
.pytest_cache/
|
|
60
|
+
cover/
|
|
61
|
+
|
|
62
|
+
# Translations
|
|
63
|
+
*.mo
|
|
64
|
+
*.pot
|
|
65
|
+
|
|
66
|
+
# Django stuff:
|
|
67
|
+
*.log
|
|
68
|
+
local_settings.py
|
|
69
|
+
db.sqlite3
|
|
70
|
+
db.sqlite3-journal
|
|
71
|
+
|
|
72
|
+
# Flask stuff:
|
|
73
|
+
instance/
|
|
74
|
+
.webassets-cache
|
|
75
|
+
|
|
76
|
+
# Scrapy stuff:
|
|
77
|
+
.scrapy
|
|
78
|
+
|
|
79
|
+
# Sphinx documentation
|
|
80
|
+
docs/_build/
|
|
81
|
+
|
|
82
|
+
# PyBuilder
|
|
83
|
+
.pybuilder/
|
|
84
|
+
target/
|
|
85
|
+
|
|
86
|
+
# Jupyter Notebook
|
|
87
|
+
.ipynb_checkpoints
|
|
88
|
+
|
|
89
|
+
# IPython
|
|
90
|
+
profile_default/
|
|
91
|
+
ipython_config.py
|
|
92
|
+
|
|
93
|
+
# pyenv
|
|
94
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
95
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
96
|
+
# .python-version
|
|
97
|
+
|
|
98
|
+
# pipenv
|
|
99
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
100
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
101
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
102
|
+
# install all needed dependencies.
|
|
103
|
+
#Pipfile.lock
|
|
104
|
+
|
|
105
|
+
# UV
|
|
106
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
107
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
108
|
+
# commonly ignored for libraries.
|
|
109
|
+
#uv.lock
|
|
110
|
+
|
|
111
|
+
# poetry
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
113
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
114
|
+
# commonly ignored for libraries.
|
|
115
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
116
|
+
#poetry.lock
|
|
117
|
+
#poetry.toml
|
|
118
|
+
|
|
119
|
+
# pdm
|
|
120
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
121
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
122
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
123
|
+
#pdm.lock
|
|
124
|
+
#pdm.toml
|
|
125
|
+
.pdm-python
|
|
126
|
+
.pdm-build/
|
|
127
|
+
|
|
128
|
+
# pixi
|
|
129
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
130
|
+
#pixi.lock
|
|
131
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
132
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
133
|
+
.pixi
|
|
134
|
+
|
|
135
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
136
|
+
__pypackages__/
|
|
137
|
+
|
|
138
|
+
# Celery stuff
|
|
139
|
+
celerybeat-schedule
|
|
140
|
+
celerybeat.pid
|
|
141
|
+
|
|
142
|
+
# SageMath parsed files
|
|
143
|
+
*.sage.py
|
|
144
|
+
|
|
145
|
+
# Environments
|
|
146
|
+
.env
|
|
147
|
+
.envrc
|
|
148
|
+
.venv
|
|
149
|
+
env/
|
|
150
|
+
venv/
|
|
151
|
+
ENV/
|
|
152
|
+
env.bak/
|
|
153
|
+
venv.bak/
|
|
154
|
+
|
|
155
|
+
# Spyder project settings
|
|
156
|
+
.spyderproject
|
|
157
|
+
.spyproject
|
|
158
|
+
|
|
159
|
+
# Rope project settings
|
|
160
|
+
.ropeproject
|
|
161
|
+
|
|
162
|
+
# mkdocs documentation
|
|
163
|
+
/site
|
|
164
|
+
|
|
165
|
+
# mypy
|
|
166
|
+
.mypy_cache/
|
|
167
|
+
.dmypy.json
|
|
168
|
+
dmypy.json
|
|
169
|
+
|
|
170
|
+
# Pyre type checker
|
|
171
|
+
.pyre/
|
|
172
|
+
|
|
173
|
+
# pytype static type analyzer
|
|
174
|
+
.pytype/
|
|
175
|
+
|
|
176
|
+
# Cython debug symbols
|
|
177
|
+
cython_debug/
|
|
178
|
+
|
|
179
|
+
# PyCharm
|
|
180
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
181
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
182
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
183
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
184
|
+
#.idea/
|
|
185
|
+
|
|
186
|
+
# Abstra
|
|
187
|
+
# Abstra is an AI-powered process automation framework.
|
|
188
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
189
|
+
# Learn more at https://abstra.io/docs
|
|
190
|
+
.abstra/
|
|
191
|
+
|
|
192
|
+
# Visual Studio Code
|
|
193
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
194
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
195
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
196
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
197
|
+
# .vscode/
|
|
198
|
+
|
|
199
|
+
# Ruff stuff:
|
|
200
|
+
.ruff_cache/
|
|
201
|
+
|
|
202
|
+
# PyPI configuration file
|
|
203
|
+
.pypirc
|
|
204
|
+
|
|
205
|
+
# Cursor
|
|
206
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
207
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
208
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
209
|
+
.cursorignore
|
|
210
|
+
.cursorindexingignore
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// markdownlint-cli2 configuration. Conservative ruleset — start strict,
|
|
2
|
+
// loosen only when a rule produces more noise than value.
|
|
3
|
+
{
|
|
4
|
+
"config": {
|
|
5
|
+
"default": true,
|
|
6
|
+
// MD013 (line length) is too noisy for technical writing with URLs,
|
|
7
|
+
// and we already line-wrap prose by hand.
|
|
8
|
+
"MD013": false,
|
|
9
|
+
// MD033 (no inline HTML) blocks legitimate uses like <details> and
|
|
10
|
+
// <kbd>. Enable case-by-case if a specific tag becomes a problem.
|
|
11
|
+
"MD033": false,
|
|
12
|
+
// MD041 (first line must be a top-level heading) breaks for files
|
|
13
|
+
// that lead with frontmatter (e.g. claude_memory/*.md).
|
|
14
|
+
"MD041": false
|
|
15
|
+
},
|
|
16
|
+
"globs": ["**/*.md"],
|
|
17
|
+
"ignores": [
|
|
18
|
+
".venv/**",
|
|
19
|
+
"build/**",
|
|
20
|
+
"dist/**",
|
|
21
|
+
"*.egg-info/**",
|
|
22
|
+
"node_modules/**",
|
|
23
|
+
".git/**"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# See https://pre-commit.com for more information
|
|
2
|
+
# Run `pre-commit install` once after cloning to enable hooks.
|
|
3
|
+
repos:
|
|
4
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
5
|
+
rev: v0.15.10
|
|
6
|
+
hooks:
|
|
7
|
+
- id: ruff
|
|
8
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
9
|
+
- id: ruff-format
|
|
10
|
+
|
|
11
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
12
|
+
rev: v1.20.0
|
|
13
|
+
hooks:
|
|
14
|
+
- id: mypy
|
|
15
|
+
args: [--config-file=pyproject.toml]
|
|
16
|
+
additional_dependencies:
|
|
17
|
+
- structlog>=24.0
|
|
18
|
+
- pytest>=8.0
|
|
19
|
+
pass_filenames: false
|
|
20
|
+
entry: mypy src/
|
|
21
|
+
|
|
22
|
+
- repo: https://github.com/codespell-project/codespell
|
|
23
|
+
rev: v2.3.0
|
|
24
|
+
hooks:
|
|
25
|
+
- id: codespell
|
|
26
|
+
additional_dependencies: [tomli]
|
|
27
|
+
args: [--config=.codespellrc]
|
|
28
|
+
|
|
29
|
+
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
|
30
|
+
rev: v0.14.0
|
|
31
|
+
hooks:
|
|
32
|
+
- id: markdownlint-cli2
|
|
33
|
+
|
|
34
|
+
- repo: local
|
|
35
|
+
hooks:
|
|
36
|
+
- id: pytest
|
|
37
|
+
name: pytest
|
|
38
|
+
entry: python -m pytest --no-header -q
|
|
39
|
+
language: system
|
|
40
|
+
pass_filenames: false
|
|
41
|
+
always_run: true
|
|
42
|
+
stages: [pre-push]
|
pyfabric-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Instructions for AI coding assistants (Claude, Copilot, etc.) working in
|
|
4
|
+
this repository.
|
|
5
|
+
|
|
6
|
+
## Project Overview
|
|
7
|
+
|
|
8
|
+
pyfabric is a Python library for programmatically creating, validating,
|
|
9
|
+
and locally testing Microsoft Fabric items that are compatible with Fabric
|
|
10
|
+
git sync. Target users are AI coding assistants and developers who need to
|
|
11
|
+
generate and test Fabric item definitions locally.
|
|
12
|
+
|
|
13
|
+
## What pyfabric is — and is not
|
|
14
|
+
|
|
15
|
+
pyfabric is a **dev-time / build-time** tool. It runs on the developer's
|
|
16
|
+
machine (or in CI), not inside Fabric. Its three jobs are:
|
|
17
|
+
|
|
18
|
+
1. **Create** Fabric artifacts (notebooks, semantic models, reports,
|
|
19
|
+
variable libraries, lakehouse DDL) as files committed to a
|
|
20
|
+
git-synced workspace repo.
|
|
21
|
+
2. **Test locally** — DuckDB lakehouse mock, OneLake helpers, schema
|
|
22
|
+
validation, transform logic — without standing up Fabric.
|
|
23
|
+
3. **Manage Fabric operationally** from outside the platform — trigger
|
|
24
|
+
notebook runs via the Jobs API, refresh semantic models, audit
|
|
25
|
+
workspaces.
|
|
26
|
+
|
|
27
|
+
**pyfabric is NOT a notebook runtime dependency.** A Fabric Spark
|
|
28
|
+
notebook should not `import pyfabric`. Several past sessions have
|
|
29
|
+
wasted time trying to install pyfabric inside Fabric (in
|
|
30
|
+
`Resources/builtin/`, in custom Environments, via `%pip install
|
|
31
|
+
pyfabric` cells). The right pattern is:
|
|
32
|
+
|
|
33
|
+
- Notebooks stay thin — orchestration only.
|
|
34
|
+
- The project's transform / DDL / SQL helpers live in a project
|
|
35
|
+
wheel (e.g. `fabric_utils-*.whl`) shipped via
|
|
36
|
+
`Resources/builtin/` (see `claude_memory/notebook_wheel_resources_pattern.md`).
|
|
37
|
+
- That project wheel depends only on Fabric Spark runtime
|
|
38
|
+
pre-installed packages (`azure-identity`,
|
|
39
|
+
`azure-storage-file-datalake`, `pyarrow`, `requests`), not on
|
|
40
|
+
pyfabric.
|
|
41
|
+
- pyfabric stays on the dev machine, where it builds and validates
|
|
42
|
+
the notebook + the project wheel.
|
|
43
|
+
|
|
44
|
+
When you find yourself reaching for pyfabric inside a notebook,
|
|
45
|
+
that's a sign the logic should move into the project wheel where
|
|
46
|
+
it's locally testable; the notebook then calls the wheel.
|
|
47
|
+
|
|
48
|
+
## Technical Stack
|
|
49
|
+
|
|
50
|
+
- **Python**: 3.11+ (use modern syntax: `match`, `X | Y` unions, `Self` from `typing`). Avoid 3.12-only syntax — Fabric Spark runtime 1.3 is 3.11. Specifically: do NOT use `type X = Y` (PEP 695, 3.12+) or `class Foo[T]` (PEP 695, 3.12+).
|
|
51
|
+
- **Build**: hatchling with hatch-vcs (version from git tags)
|
|
52
|
+
- **Layout**: src-layout (`src/pyfabric/`)
|
|
53
|
+
- **Linting**: ruff (configured in pyproject.toml)
|
|
54
|
+
- **Type checking**: mypy in strict mode
|
|
55
|
+
- **Logging**: structlog (JSON output, token masking, context binding)
|
|
56
|
+
- **Testing**: pytest (tests in `tests/`)
|
|
57
|
+
- **CI**: GitHub Actions (lint, type-check, test, dependency review)
|
|
58
|
+
|
|
59
|
+
## Sub-package Structure
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
src/pyfabric/
|
|
63
|
+
client/ — Auth, REST API, HTTP client for Fabric service
|
|
64
|
+
items/ — Create, load, save, validate Fabric item definitions
|
|
65
|
+
data/ — OneLake DFS, SQL connections, lakehouse table I/O
|
|
66
|
+
workspace/ — Workspace-level operations
|
|
67
|
+
testing/ — pytest fixtures, DuckDB Spark mock for users
|
|
68
|
+
cli.py — CLI entry point
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Key Conventions
|
|
72
|
+
|
|
73
|
+
- All code must pass `ruff check`, `ruff format --check`, and `mypy --strict`
|
|
74
|
+
- All public functions and classes must have type annotations
|
|
75
|
+
- Use `X | None` not `typing.Optional[X]`; use `X | Y` not `typing.Union`
|
|
76
|
+
- Use `list`, `dict`, `tuple` not `typing.List`, `typing.Dict`, `typing.Tuple`
|
|
77
|
+
- `from __future__ import annotations` is NOT needed (Python 3.11+ supports `X | Y` unions natively via PEP 604)
|
|
78
|
+
- Prefer `pathlib.Path` over `os.path`
|
|
79
|
+
- Prefer dataclasses or named tuples over plain dicts for structured data
|
|
80
|
+
- Tests use pytest fixtures; avoid unittest.TestCase
|
|
81
|
+
- No mutable default arguments
|
|
82
|
+
- Use `import structlog` and `log = structlog.get_logger()` (NOT stdlib `logging`)
|
|
83
|
+
- Use `log.info("message", key=value)` for structured context (NOT f-strings in messages)
|
|
84
|
+
|
|
85
|
+
## Running Checks
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ruff check . # Lint
|
|
89
|
+
ruff format --check . # Format check
|
|
90
|
+
mypy src/ # Type check
|
|
91
|
+
pytest # Run tests
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Version Management
|
|
95
|
+
|
|
96
|
+
Version is derived from git tags via hatch-vcs. Do NOT manually edit
|
|
97
|
+
version strings. The file `src/pyfabric/_version.py` is auto-generated
|
|
98
|
+
and must not be committed.
|
|
99
|
+
|
|
100
|
+
## Dependencies
|
|
101
|
+
|
|
102
|
+
- Runtime dependencies go in `[project] dependencies` in pyproject.toml
|
|
103
|
+
- Optional dependency groups: `[azure]`, `[data]`, `[testing]`, `[all]`, `[dev]`
|
|
104
|
+
- Keep runtime dependencies minimal — heavy deps belong in optional groups
|
|
105
|
+
|
|
106
|
+
## Fabric Item Structure (git sync format)
|
|
107
|
+
|
|
108
|
+
All Fabric items follow this directory structure:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
{DisplayName}.{ItemType}/
|
|
112
|
+
.platform # Required: metadata + logicalId (UUID)
|
|
113
|
+
{definition_files...} # Item-specific content
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The `.platform` file uses schema version 2.0:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"$schema": "https://developer.microsoft.com/json-schemas/fabric/gitIntegration/platformProperties/2.0.0/schema.json",
|
|
121
|
+
"metadata": {
|
|
122
|
+
"type": "{ItemType}",
|
|
123
|
+
"displayName": "{DisplayName}"
|
|
124
|
+
},
|
|
125
|
+
"config": {
|
|
126
|
+
"version": "2.0",
|
|
127
|
+
"logicalId": "{UUID}"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Supported item types: Notebook, Lakehouse, Dataflow, Environment,
|
|
133
|
+
VariableLibrary, SemanticModel, Report, DataPipeline, Warehouse,
|
|
134
|
+
MirroredDatabase, Map.
|
|
135
|
+
|
|
136
|
+
## Provisioning Workflow (git-first)
|
|
137
|
+
|
|
138
|
+
pyfabric ships two complementary ways to create Fabric items:
|
|
139
|
+
|
|
140
|
+
1. **Builder → git artifact** (`NotebookBuilder.save_to_disk()`,
|
|
141
|
+
`MirroredDatabaseBuilder.save_to_disk()`, etc.) writes a
|
|
142
|
+
`.platform` + definition files to a folder under your
|
|
143
|
+
git-synced workspace repo. **This is the primary path.**
|
|
144
|
+
2. **REST helpers** (`create_item`, `create_mirrored_database`, …)
|
|
145
|
+
call the Fabric REST API directly. **Use only for scripted
|
|
146
|
+
automation in workspaces that are not git-synced.**
|
|
147
|
+
|
|
148
|
+
Mixing the two on the same workspace causes a duplicate-item
|
|
149
|
+
conflict: the REST-created item and the later git-synced item live
|
|
150
|
+
under different IDs, and Fabric will not merge them. The
|
|
151
|
+
git-synced item also receives a fresh ID, breaking any committed
|
|
152
|
+
references (e.g. notebook parameters that hard-code the mirror's
|
|
153
|
+
GUID).
|
|
154
|
+
|
|
155
|
+
### Recommended order for a git-synced workspace
|
|
156
|
+
|
|
157
|
+
1. Generate the artifact locally with the builder.
|
|
158
|
+
2. Run local tests (producer logic, schema-compat, artifact shape,
|
|
159
|
+
`validate_workspace`).
|
|
160
|
+
3. Commit + push to the workspace's git-synced branch.
|
|
161
|
+
4. Have a human trigger the **manual git-sync** in the Fabric portal
|
|
162
|
+
(and bind any required OAuth credentials on first refresh — see
|
|
163
|
+
`claude_memory/first_refresh_cred_binding.md`).
|
|
164
|
+
5. Only then perform data-plane mutations (upload landing-zone
|
|
165
|
+
files, `start_mirroring`, `update_definition`, …) — these
|
|
166
|
+
target the just-synced item by its real GUID.
|
|
167
|
+
|
|
168
|
+
AI assistants using pyfabric in a git-synced workspace should
|
|
169
|
+
**pause after step 3** and wait for the user to confirm the sync
|
|
170
|
+
before issuing any REST mutation that would create or modify an
|
|
171
|
+
item.
|
|
172
|
+
|
|
173
|
+
## What NOT to Do
|
|
174
|
+
|
|
175
|
+
- Do not add `__version__ = "..."` manually anywhere
|
|
176
|
+
- Do not create setup.py or setup.cfg
|
|
177
|
+
- Do not commit `src/pyfabric/_version.py`
|
|
178
|
+
- Do not use `datetime.utcnow()` — use `datetime.now(timezone.utc)`
|