audio-as-code 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.
- audio_as_code-0.1.0/.github/FUNDING.yml +1 -0
- audio_as_code-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +42 -0
- audio_as_code-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- audio_as_code-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- audio_as_code-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +18 -0
- audio_as_code-0.1.0/.github/dependabot.yml +29 -0
- audio_as_code-0.1.0/.github/scripts/check_distribution.py +272 -0
- audio_as_code-0.1.0/.github/scripts/check_schema.py +11 -0
- audio_as_code-0.1.0/.github/scripts/smoke_distribution.py +80 -0
- audio_as_code-0.1.0/.github/scripts/smoke_installed.py +112 -0
- audio_as_code-0.1.0/.github/workflows/ci.yml +71 -0
- audio_as_code-0.1.0/.github/workflows/publish.yml +141 -0
- audio_as_code-0.1.0/.gitignore +56 -0
- audio_as_code-0.1.0/AGENTS.md +8 -0
- audio_as_code-0.1.0/CHANGELOG.md +107 -0
- audio_as_code-0.1.0/CONTRIBUTING.md +135 -0
- audio_as_code-0.1.0/LICENSE +21 -0
- audio_as_code-0.1.0/PKG-INFO +340 -0
- audio_as_code-0.1.0/README.md +312 -0
- audio_as_code-0.1.0/SECURITY.md +36 -0
- audio_as_code-0.1.0/docs/agents.md +192 -0
- audio_as_code-0.1.0/docs/analytics.md +153 -0
- audio_as_code-0.1.0/docs/architecture.md +124 -0
- audio_as_code-0.1.0/docs/classic-reimaginations.md +91 -0
- audio_as_code-0.1.0/docs/classic-showcase.md +80 -0
- audio_as_code-0.1.0/docs/cloudflare-hosting.md +162 -0
- audio_as_code-0.1.0/docs/composition-tools.md +83 -0
- audio_as_code-0.1.0/docs/creative-workflows.md +119 -0
- audio_as_code-0.1.0/docs/expressive-engine.md +171 -0
- audio_as_code-0.1.0/docs/extended-instruments.md +181 -0
- audio_as_code-0.1.0/docs/full-compositions.md +75 -0
- audio_as_code-0.1.0/docs/instrument-foundation.md +95 -0
- audio_as_code-0.1.0/docs/music-demos.md +83 -0
- audio_as_code-0.1.0/docs/orchestra.md +105 -0
- audio_as_code-0.1.0/docs/piano-sustain.md +74 -0
- audio_as_code-0.1.0/docs/releasing.md +154 -0
- audio_as_code-0.1.0/docs/render-performance.md +87 -0
- audio_as_code-0.1.0/docs/score-inspection.md +122 -0
- audio_as_code-0.1.0/docs/site/agents.md +240 -0
- audio_as_code-0.1.0/docs/site/composition.md +359 -0
- audio_as_code-0.1.0/docs/site/examples/01_first_score.py +65 -0
- audio_as_code-0.1.0/docs/site/examples/02_motif_and_progression.py +104 -0
- audio_as_code-0.1.0/docs/site/examples/03_song_form.py +170 -0
- audio_as_code-0.1.0/docs/site/examples/04_expressive_controls.py +126 -0
- audio_as_code-0.1.0/docs/site/examples/05_agent_loop.py +117 -0
- audio_as_code-0.1.0/docs/site/examples/06_agent_handoff.py +94 -0
- audio_as_code-0.1.0/docs/site/examples/agent-score.json +86 -0
- audio_as_code-0.1.0/docs/site/integrations.md +189 -0
- audio_as_code-0.1.0/docs/site/llms.txt +80 -0
- audio_as_code-0.1.0/docs/site/quickstart.md +217 -0
- audio_as_code-0.1.0/docs/site/reference.md +240 -0
- audio_as_code-0.1.0/docs/synthesis.md +142 -0
- audio_as_code-0.1.0/docs/website-discovery.md +38 -0
- audio_as_code-0.1.0/docs/website.md +192 -0
- audio_as_code-0.1.0/examples/_site_discovery.py +138 -0
- audio_as_code-0.1.0/examples/benchmark_render.py +203 -0
- audio_as_code-0.1.0/examples/build_site.py +1500 -0
- audio_as_code-0.1.0/examples/classic_reimaginations.py +522 -0
- audio_as_code-0.1.0/examples/classic_showcase.py +305 -0
- audio_as_code-0.1.0/examples/composition_tools.py +44 -0
- audio_as_code-0.1.0/examples/creative_workflows.py +393 -0
- audio_as_code-0.1.0/examples/expressive-score.json +303 -0
- audio_as_code-0.1.0/examples/expressive_engine.py +130 -0
- audio_as_code-0.1.0/examples/famous_music.py +309 -0
- audio_as_code-0.1.0/examples/first_light.py +17 -0
- audio_as_code-0.1.0/examples/full_compositions.py +616 -0
- audio_as_code-0.1.0/examples/instrument_browser.py +347 -0
- audio_as_code-0.1.0/examples/music/README.md +30 -0
- audio_as_code-0.1.0/examples/music/build_classics_full.py +374 -0
- audio_as_code-0.1.0/examples/music/build_excerpts.py +292 -0
- audio_as_code-0.1.0/examples/music/classics-full.json +29757 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007-viola.ly +64 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007.ly +61 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_allemande.ly +52 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_courante.ly +63 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_gigue.ly +50 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_menuet_i.ly +20 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_menuet_ii.ly +20 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_prelude.ly +60 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_sarabande.ly +29 -0
- audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-header.ily +25 -0
- audio_as_code-0.1.0/examples/music/classics-sources/fur_elise-fur_elise.ly +203 -0
- audio_as_code-0.1.0/examples/music/classics-sources/greensleeves-greensleeves.ly +73 -0
- audio_as_code-0.1.0/examples/music/classics-sources/ode_to_joy-ode_to_joy.ly +98 -0
- audio_as_code-0.1.0/examples/music/classics-sources/turkish_march-KV331_3_RondoAllaTurca.ly +43 -0
- audio_as_code-0.1.0/examples/music/classics-sources/turkish_march-KV331_3_RondoAllaTurcaNotes.ly +321 -0
- audio_as_code-0.1.0/examples/music/excerpts.json +2637 -0
- audio_as_code-0.1.0/examples/physical_instruments.py +212 -0
- audio_as_code-0.1.0/examples/piano_sustain.py +87 -0
- audio_as_code-0.1.0/examples/prepare_cloudflare.py +147 -0
- audio_as_code-0.1.0/examples/serve_site.py +173 -0
- audio_as_code-0.1.0/examples/showcase.py +438 -0
- audio_as_code-0.1.0/pyproject.toml +79 -0
- audio_as_code-0.1.0/schemas/song-v1.schema.json +607 -0
- audio_as_code-0.1.0/skills/audio-as-code/SKILL.md +195 -0
- audio_as_code-0.1.0/src/audio_as_code/__init__.py +44 -0
- audio_as_code-0.1.0/src/audio_as_code/__main__.py +3 -0
- audio_as_code-0.1.0/src/audio_as_code/_audio.py +87 -0
- audio_as_code-0.1.0/src/audio_as_code/_export_rules.py +32 -0
- audio_as_code-0.1.0/src/audio_as_code/_orchestra_profiles.py +188 -0
- audio_as_code-0.1.0/src/audio_as_code/_paths.py +36 -0
- audio_as_code-0.1.0/src/audio_as_code/_voices.py +173 -0
- audio_as_code-0.1.0/src/audio_as_code/acoustics.py +126 -0
- audio_as_code-0.1.0/src/audio_as_code/automation.py +40 -0
- audio_as_code-0.1.0/src/audio_as_code/cli.py +171 -0
- audio_as_code-0.1.0/src/audio_as_code/demo.py +106 -0
- audio_as_code-0.1.0/src/audio_as_code/effects.py +85 -0
- audio_as_code-0.1.0/src/audio_as_code/extended.py +283 -0
- audio_as_code-0.1.0/src/audio_as_code/inspection.py +351 -0
- audio_as_code-0.1.0/src/audio_as_code/instruments.py +923 -0
- audio_as_code-0.1.0/src/audio_as_code/midi.py +192 -0
- audio_as_code-0.1.0/src/audio_as_code/model.py +292 -0
- audio_as_code-0.1.0/src/audio_as_code/orchestra.py +481 -0
- audio_as_code-0.1.0/src/audio_as_code/pattern.py +151 -0
- audio_as_code-0.1.0/src/audio_as_code/physical.py +189 -0
- audio_as_code-0.1.0/src/audio_as_code/py.typed +0 -0
- audio_as_code-0.1.0/src/audio_as_code/render.py +230 -0
- audio_as_code-0.1.0/tests/test_acoustics.py +68 -0
- audio_as_code-0.1.0/tests/test_agent_handoff.py +55 -0
- audio_as_code-0.1.0/tests/test_agent_loop.py +57 -0
- audio_as_code-0.1.0/tests/test_agent_readiness.py +154 -0
- audio_as_code-0.1.0/tests/test_classic_showcase.py +63 -0
- audio_as_code-0.1.0/tests/test_cli_dx.py +72 -0
- audio_as_code-0.1.0/tests/test_cloudflare_staging.py +99 -0
- audio_as_code-0.1.0/tests/test_core_export_regressions.py +96 -0
- audio_as_code-0.1.0/tests/test_creative_workflows.py +117 -0
- audio_as_code-0.1.0/tests/test_demo_performance.py +144 -0
- audio_as_code-0.1.0/tests/test_distribution_contract.py +254 -0
- audio_as_code-0.1.0/tests/test_expressive_engine.py +331 -0
- audio_as_code-0.1.0/tests/test_extended_instruments.py +249 -0
- audio_as_code-0.1.0/tests/test_finite_strikes.py +66 -0
- audio_as_code-0.1.0/tests/test_full_compositions.py +120 -0
- audio_as_code-0.1.0/tests/test_instrument_refinement.py +132 -0
- audio_as_code-0.1.0/tests/test_instruments.py +156 -0
- audio_as_code-0.1.0/tests/test_midi_cli.py +106 -0
- audio_as_code-0.1.0/tests/test_midi_warnings.py +84 -0
- audio_as_code-0.1.0/tests/test_music_demos.py +54 -0
- audio_as_code-0.1.0/tests/test_orchestra.py +239 -0
- audio_as_code-0.1.0/tests/test_output_preflight.py +42 -0
- audio_as_code-0.1.0/tests/test_pattern_composition.py +147 -0
- audio_as_code-0.1.0/tests/test_physical.py +146 -0
- audio_as_code-0.1.0/tests/test_piano_sustain.py +243 -0
- audio_as_code-0.1.0/tests/test_render.py +104 -0
- audio_as_code-0.1.0/tests/test_render_performance.py +31 -0
- audio_as_code-0.1.0/tests/test_score.py +105 -0
- audio_as_code-0.1.0/tests/test_score_inspection.py +255 -0
- audio_as_code-0.1.0/tests/test_security_regressions.py +60 -0
- audio_as_code-0.1.0/tests/test_serve_site.py +107 -0
- audio_as_code-0.1.0/tests/test_site_analytics.py +241 -0
- audio_as_code-0.1.0/tests/test_site_discovery.py +66 -0
- audio_as_code-0.1.0/tests/test_site_docs.py +68 -0
- audio_as_code-0.1.0/uv.lock +895 -0
- audio_as_code-0.1.0/web/cloudflare/media-handler.js +64 -0
- audio_as_code-0.1.0/web/cloudflare/media-handler.test.js +85 -0
- audio_as_code-0.1.0/web/cloudflare/package-lock.json +1590 -0
- audio_as_code-0.1.0/web/cloudflare/package.json +12 -0
- audio_as_code-0.1.0/web/cloudflare/worker.js +12 -0
- audio_as_code-0.1.0/web/instrument-browser.html +586 -0
- audio_as_code-0.1.0/web/site/404.html +11 -0
- audio_as_code-0.1.0/web/site/analytics-config.json +9 -0
- audio_as_code-0.1.0/web/site/assets/agent-handoff.js +89 -0
- audio_as_code-0.1.0/web/site/assets/analytics.js +169 -0
- audio_as_code-0.1.0/web/site/assets/fonts/Anybody-OFL.txt +93 -0
- audio_as_code-0.1.0/web/site/assets/fonts/Archivo-OFL.txt +93 -0
- audio_as_code-0.1.0/web/site/assets/fonts/FONTS.txt +5 -0
- audio_as_code-0.1.0/web/site/assets/fonts/JetBrainsMono-OFL.txt +93 -0
- audio_as_code-0.1.0/web/site/assets/fonts/anybody-latin.woff2 +0 -0
- audio_as_code-0.1.0/web/site/assets/fonts/archivo-latin.woff2 +0 -0
- audio_as_code-0.1.0/web/site/assets/fonts/jetbrains-mono-latin.woff2 +0 -0
- audio_as_code-0.1.0/web/site/assets/icon.svg +1 -0
- audio_as_code-0.1.0/web/site/assets/ring.svg +1 -0
- audio_as_code-0.1.0/web/site/assets/site.css +719 -0
- audio_as_code-0.1.0/web/site/assets/site.js +938 -0
- audio_as_code-0.1.0/web/site/assets/social.png +0 -0
- audio_as_code-0.1.0/web/site/assets/social.svg +18 -0
- audio_as_code-0.1.0/web/site/assets/torn.svg +1 -0
- audio_as_code-0.1.0/web/site/identity.json +8 -0
- audio_as_code-0.1.0/web/site/index.html +230 -0
- audio_as_code-0.1.0/web/site/layout.html +62 -0
- audio_as_code-0.1.0/web/site/snippets/loop.py +20 -0
- audio_as_code-0.1.0/web/site/source.html +30 -0
- audio_as_code-0.1.0/web/site/tools/verify_site.py +345 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ko_fi: joaothecarvalho
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Report a reproducible score, rendering, export, or CLI problem.
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
This is an early procedural-audio prototype. Please use a minimal original
|
|
9
|
+
or public-domain score. Do not include credentials or private project files.
|
|
10
|
+
For a suspected vulnerability, read [SECURITY.md](https://github.com/joaoCarvalho1000/audio-as-code/blob/main/SECURITY.md) before sharing details.
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: expected
|
|
13
|
+
attributes:
|
|
14
|
+
label: Expected and actual behavior
|
|
15
|
+
description: What did you expect, and what happened instead?
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
- type: textarea
|
|
19
|
+
id: reproduction
|
|
20
|
+
attributes:
|
|
21
|
+
label: Smallest reproduction
|
|
22
|
+
description: Paste the command and a small JSON score or Python example.
|
|
23
|
+
render: text
|
|
24
|
+
validations:
|
|
25
|
+
required: true
|
|
26
|
+
- type: textarea
|
|
27
|
+
id: environment
|
|
28
|
+
attributes:
|
|
29
|
+
label: Environment
|
|
30
|
+
description: Include your OS, Python version, aac --version, and installation method.
|
|
31
|
+
placeholder: Source checkout or source ZIP; Python version; OS; package version.
|
|
32
|
+
validations:
|
|
33
|
+
required: true
|
|
34
|
+
- type: textarea
|
|
35
|
+
id: diagnostics
|
|
36
|
+
attributes:
|
|
37
|
+
label: Diagnostics and audio observations
|
|
38
|
+
description: |
|
|
39
|
+
Include the complete JSON error or relevant render/analyze report. For sound
|
|
40
|
+
problems, identify the instrument, pitch, tempo, and what you heard. State
|
|
41
|
+
whether you listened; signal measurements alone do not establish sound quality.
|
|
42
|
+
render: text
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Describe a composition workflow or API capability you need.
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: task
|
|
7
|
+
attributes:
|
|
8
|
+
label: What are you trying to make?
|
|
9
|
+
description: Describe the musical or integration task and what currently gets in the way.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: Proposed behavior
|
|
16
|
+
description: A small score, command, or Python example helps make the request concrete.
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: alternatives
|
|
21
|
+
attributes:
|
|
22
|
+
label: Current workaround and constraints
|
|
23
|
+
description: Include compatibility needs, timing, instrument controls, or export requirements.
|
|
24
|
+
- type: markdown
|
|
25
|
+
attributes:
|
|
26
|
+
value: |
|
|
27
|
+
Instrument sources must be generated from code. Recorded samples, SoundFonts,
|
|
28
|
+
and measured impulse responses are outside this project's synthesis direction.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## What changes?
|
|
2
|
+
|
|
3
|
+
Describe the concrete problem and resulting behavior. Include a small before/after
|
|
4
|
+
example when it helps someone review the change.
|
|
5
|
+
|
|
6
|
+
## Validation
|
|
7
|
+
|
|
8
|
+
List the commands you ran and their results. Mention checks you could not run.
|
|
9
|
+
For audio changes, state what you listened to and keep numerical checks separate
|
|
10
|
+
from perceptual observations.
|
|
11
|
+
|
|
12
|
+
- [ ] Relevant tests, Ruff lint, and formatting pass.
|
|
13
|
+
- [ ] User-facing behavior and compatibility changes are documented.
|
|
14
|
+
- [ ] Score schema regenerated if the models changed.
|
|
15
|
+
- [ ] New instrument controls affect implemented synthesis and are tested.
|
|
16
|
+
- [ ] No generated output, credentials, or workstation state is included.
|
|
17
|
+
|
|
18
|
+
Mark checks that do not apply as such; see CONTRIBUTING.md for the full workflow.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: uv
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: monthly
|
|
7
|
+
open-pull-requests-limit: 2
|
|
8
|
+
groups:
|
|
9
|
+
python-minor-patch:
|
|
10
|
+
patterns: ['*']
|
|
11
|
+
update-types: [minor, patch]
|
|
12
|
+
- package-ecosystem: npm
|
|
13
|
+
directory: /web/cloudflare
|
|
14
|
+
schedule:
|
|
15
|
+
interval: monthly
|
|
16
|
+
open-pull-requests-limit: 2
|
|
17
|
+
groups:
|
|
18
|
+
hosting-minor-patch:
|
|
19
|
+
patterns: ['*']
|
|
20
|
+
update-types: [minor, patch]
|
|
21
|
+
- package-ecosystem: github-actions
|
|
22
|
+
directory: /
|
|
23
|
+
schedule:
|
|
24
|
+
interval: monthly
|
|
25
|
+
open-pull-requests-limit: 2
|
|
26
|
+
groups:
|
|
27
|
+
actions-minor-patch:
|
|
28
|
+
patterns: ['*']
|
|
29
|
+
update-types: [minor, patch]
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Inspect release artifacts without importing or extracting their contents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import ast
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import stat
|
|
10
|
+
import tarfile
|
|
11
|
+
import zipfile
|
|
12
|
+
from email.parser import BytesParser
|
|
13
|
+
from pathlib import Path, PurePosixPath
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
16
|
+
PRIVATE_PARTS = {
|
|
17
|
+
".git",
|
|
18
|
+
".env",
|
|
19
|
+
".agents",
|
|
20
|
+
".codex",
|
|
21
|
+
".impeccable",
|
|
22
|
+
".wrangler",
|
|
23
|
+
".claude",
|
|
24
|
+
".orca",
|
|
25
|
+
".cursor",
|
|
26
|
+
".idea",
|
|
27
|
+
".vscode",
|
|
28
|
+
".internal",
|
|
29
|
+
".venv",
|
|
30
|
+
"node_modules",
|
|
31
|
+
"__pycache__",
|
|
32
|
+
".pytest_cache",
|
|
33
|
+
".ruff_cache",
|
|
34
|
+
".mypy_cache",
|
|
35
|
+
".hypothesis",
|
|
36
|
+
".tox",
|
|
37
|
+
".nox",
|
|
38
|
+
"playwright-report",
|
|
39
|
+
"test-results",
|
|
40
|
+
"htmlcov",
|
|
41
|
+
"%systemdrive%",
|
|
42
|
+
"output",
|
|
43
|
+
"dist",
|
|
44
|
+
"build",
|
|
45
|
+
}
|
|
46
|
+
PRIVATE_SUFFIXES = {
|
|
47
|
+
".pyc",
|
|
48
|
+
".pyo",
|
|
49
|
+
".log",
|
|
50
|
+
".key",
|
|
51
|
+
".pem",
|
|
52
|
+
".p12",
|
|
53
|
+
".pfx",
|
|
54
|
+
".prof",
|
|
55
|
+
".wav",
|
|
56
|
+
".mp3",
|
|
57
|
+
".mid",
|
|
58
|
+
".midi",
|
|
59
|
+
}
|
|
60
|
+
INTERNAL_DOCS = {
|
|
61
|
+
"design.md",
|
|
62
|
+
"product.md",
|
|
63
|
+
"plan.md",
|
|
64
|
+
"handoff.md",
|
|
65
|
+
"session.md",
|
|
66
|
+
"notes.md",
|
|
67
|
+
"progress.md",
|
|
68
|
+
"checkpoint.md",
|
|
69
|
+
"tasks.md",
|
|
70
|
+
"instrument-browser-brief.md",
|
|
71
|
+
"instrument-refinement.md",
|
|
72
|
+
}
|
|
73
|
+
SDIST_ROOTS = {
|
|
74
|
+
"src",
|
|
75
|
+
"tests",
|
|
76
|
+
"examples",
|
|
77
|
+
"docs",
|
|
78
|
+
"schemas",
|
|
79
|
+
"skills",
|
|
80
|
+
"web",
|
|
81
|
+
".github",
|
|
82
|
+
"README.md",
|
|
83
|
+
"LICENSE",
|
|
84
|
+
"CONTRIBUTING.md",
|
|
85
|
+
"AGENTS.md",
|
|
86
|
+
"CHANGELOG.md",
|
|
87
|
+
"SECURITY.md",
|
|
88
|
+
".gitignore",
|
|
89
|
+
"pyproject.toml",
|
|
90
|
+
"uv.lock",
|
|
91
|
+
"PKG-INFO",
|
|
92
|
+
}
|
|
93
|
+
REQUIRED_SOURCE = {
|
|
94
|
+
"src/audio_as_code/__init__.py",
|
|
95
|
+
"src/audio_as_code/py.typed",
|
|
96
|
+
"src/audio_as_code/extended.py",
|
|
97
|
+
"src/audio_as_code/_audio.py",
|
|
98
|
+
"src/audio_as_code/_voices.py",
|
|
99
|
+
"src/audio_as_code/_export_rules.py",
|
|
100
|
+
"src/audio_as_code/_orchestra_profiles.py",
|
|
101
|
+
"pyproject.toml",
|
|
102
|
+
"uv.lock",
|
|
103
|
+
"README.md",
|
|
104
|
+
"LICENSE",
|
|
105
|
+
"CONTRIBUTING.md",
|
|
106
|
+
"CHANGELOG.md",
|
|
107
|
+
"SECURITY.md",
|
|
108
|
+
"docs/architecture.md",
|
|
109
|
+
"docs/extended-instruments.md",
|
|
110
|
+
"docs/synthesis.md",
|
|
111
|
+
"docs/piano-sustain.md",
|
|
112
|
+
"docs/creative-workflows.md",
|
|
113
|
+
"docs/render-performance.md",
|
|
114
|
+
"docs/cloudflare-hosting.md",
|
|
115
|
+
"docs/releasing.md",
|
|
116
|
+
"schemas/song-v1.schema.json",
|
|
117
|
+
"examples/first_light.py",
|
|
118
|
+
"examples/classic_showcase.py",
|
|
119
|
+
"examples/piano_sustain.py",
|
|
120
|
+
"examples/creative_workflows.py",
|
|
121
|
+
"examples/benchmark_render.py",
|
|
122
|
+
"examples/prepare_cloudflare.py",
|
|
123
|
+
"examples/music/classics-full.json",
|
|
124
|
+
"skills/audio-as-code/SKILL.md",
|
|
125
|
+
".github/workflows/ci.yml",
|
|
126
|
+
".github/scripts/check_distribution.py",
|
|
127
|
+
".github/scripts/smoke_installed.py",
|
|
128
|
+
"web/cloudflare/worker.js",
|
|
129
|
+
"web/cloudflare/media-handler.js",
|
|
130
|
+
"web/cloudflare/package.json",
|
|
131
|
+
"web/cloudflare/package-lock.json",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def private_path(name: str) -> bool:
|
|
136
|
+
"""Reject private/generated paths at any nesting depth, including Windows spelling."""
|
|
137
|
+
normalized = name.replace("\\", "/")
|
|
138
|
+
parts = normalized.split("/")
|
|
139
|
+
if normalized.startswith("/") or ".." in parts or any(":" in part for part in parts):
|
|
140
|
+
return True
|
|
141
|
+
if normalized.lower().startswith("docs/internal/"):
|
|
142
|
+
return True
|
|
143
|
+
for part in parts:
|
|
144
|
+
lower = part.lower()
|
|
145
|
+
if lower in PRIVATE_PARTS or lower in INTERNAL_DOCS:
|
|
146
|
+
return True
|
|
147
|
+
if lower.startswith((".env", ".dev.vars", ".coverage", "credentials")):
|
|
148
|
+
return True
|
|
149
|
+
if lower.endswith(".egg-info") or PurePosixPath(lower).suffix in PRIVATE_SUFFIXES:
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def source_version(root: Path = ROOT) -> str:
|
|
155
|
+
tree = ast.parse((root / "src/audio_as_code/__init__.py").read_text(encoding="utf-8"))
|
|
156
|
+
for statement in tree.body:
|
|
157
|
+
if isinstance(statement, ast.Assign) and any(
|
|
158
|
+
isinstance(target, ast.Name) and target.id == "__version__"
|
|
159
|
+
for target in statement.targets
|
|
160
|
+
):
|
|
161
|
+
value = ast.literal_eval(statement.value)
|
|
162
|
+
if isinstance(value, str):
|
|
163
|
+
return value
|
|
164
|
+
raise ValueError("No literal __version__ in the package entry point")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def check_metadata(data: bytes, expected_version: str) -> None:
|
|
168
|
+
metadata = BytesParser().parsebytes(data)
|
|
169
|
+
if metadata["Name"] != "audio-as-code" or metadata["Version"] != expected_version:
|
|
170
|
+
raise ValueError("Distribution name/version disagree with the package's __version__")
|
|
171
|
+
if metadata["Requires-Python"] != ">=3.10":
|
|
172
|
+
raise ValueError("Unexpected Python requirement")
|
|
173
|
+
if (metadata["License-Expression"] or metadata["License"]) != "MIT":
|
|
174
|
+
raise ValueError("MIT license metadata is missing")
|
|
175
|
+
requirements = metadata.get_all("Requires-Dist", [])
|
|
176
|
+
names = {re.split(r"[<>=!~;\s\[]", item, maxsplit=1)[0].lower() for item in requirements}
|
|
177
|
+
if names != {"numpy", "pydantic", "mido"} or any("@" in item for item in requirements):
|
|
178
|
+
raise ValueError("Runtime dependencies changed or contain nonportable direct URLs")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check_wheel(path: Path, expected_version: str) -> dict:
|
|
182
|
+
with zipfile.ZipFile(path) as archive:
|
|
183
|
+
names = archive.namelist()
|
|
184
|
+
if len(names) != len(set(names)):
|
|
185
|
+
raise ValueError("Duplicate wheel members")
|
|
186
|
+
info_root = f"audio_as_code-{expected_version}.dist-info"
|
|
187
|
+
for info in archive.infolist():
|
|
188
|
+
if private_path(info.filename) or PurePosixPath(info.filename).parts[0] not in {
|
|
189
|
+
"audio_as_code",
|
|
190
|
+
info_root,
|
|
191
|
+
}:
|
|
192
|
+
raise ValueError(f"Unexpected wheel member: {info.filename}")
|
|
193
|
+
if stat.S_ISLNK(info.external_attr >> 16):
|
|
194
|
+
raise ValueError(f"Wheel may not contain symlinks: {info.filename}")
|
|
195
|
+
required = {
|
|
196
|
+
f"audio_as_code/{name}"
|
|
197
|
+
for name in (
|
|
198
|
+
"__init__.py",
|
|
199
|
+
"__main__.py",
|
|
200
|
+
"cli.py",
|
|
201
|
+
"model.py",
|
|
202
|
+
"pattern.py",
|
|
203
|
+
"render.py",
|
|
204
|
+
"extended.py",
|
|
205
|
+
"_audio.py",
|
|
206
|
+
"_voices.py",
|
|
207
|
+
"_export_rules.py",
|
|
208
|
+
"_orchestra_profiles.py",
|
|
209
|
+
"py.typed",
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
required |= {f"{info_root}/METADATA", f"{info_root}/WHEEL", f"{info_root}/entry_points.txt"}
|
|
213
|
+
if missing := required - set(names):
|
|
214
|
+
raise ValueError(f"Missing wheel files: {sorted(missing)}")
|
|
215
|
+
if not any(
|
|
216
|
+
name.startswith(info_root + "/") and name.endswith("/LICENSE") for name in names
|
|
217
|
+
):
|
|
218
|
+
raise ValueError("Wheel is missing the license file")
|
|
219
|
+
check_metadata(archive.read(f"{info_root}/METADATA"), expected_version)
|
|
220
|
+
entry_points = archive.read(f"{info_root}/entry_points.txt").decode()
|
|
221
|
+
if "aac = audio_as_code.cli:main" not in entry_points:
|
|
222
|
+
raise ValueError("Wheel is missing the aac console entry point")
|
|
223
|
+
return {"file": path.name, "files": len(names), "typed": True}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def check_sdist(path: Path, expected_version: str) -> dict:
|
|
227
|
+
with tarfile.open(path, "r:gz") as archive:
|
|
228
|
+
members = archive.getmembers()
|
|
229
|
+
prefix = f"audio_as_code-{expected_version}/"
|
|
230
|
+
names = set()
|
|
231
|
+
for member in members:
|
|
232
|
+
if member.isdir():
|
|
233
|
+
continue
|
|
234
|
+
if not member.isfile() or not member.name.startswith(prefix):
|
|
235
|
+
raise ValueError(f"Unexpected sdist member: {member.name}")
|
|
236
|
+
name = member.name[len(prefix) :]
|
|
237
|
+
if private_path(name) or PurePosixPath(name).parts[0] not in SDIST_ROOTS:
|
|
238
|
+
raise ValueError(f"Private/generated or unlisted sdist member: {name}")
|
|
239
|
+
if name in names:
|
|
240
|
+
raise ValueError(f"Duplicate sdist member: {name}")
|
|
241
|
+
names.add(name)
|
|
242
|
+
if missing := REQUIRED_SOURCE - names:
|
|
243
|
+
raise ValueError(f"Missing sdist files: {sorted(missing)}")
|
|
244
|
+
metadata = archive.extractfile(prefix + "PKG-INFO")
|
|
245
|
+
if metadata is None:
|
|
246
|
+
raise ValueError("Missing sdist metadata")
|
|
247
|
+
check_metadata(metadata.read(), expected_version)
|
|
248
|
+
return {"file": path.name, "files": len(names), "portable_source": True}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def main() -> None:
|
|
252
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
253
|
+
parser.add_argument("directory", nargs="?", type=Path, default=Path("dist"))
|
|
254
|
+
args = parser.parse_args()
|
|
255
|
+
wheels = sorted(args.directory.glob("*.whl"))
|
|
256
|
+
sdists = sorted(args.directory.glob("*.tar.gz"))
|
|
257
|
+
if len(wheels) != 1 or len(sdists) != 1:
|
|
258
|
+
parser.error("Expected exactly one wheel and one sdist; use a fresh output directory")
|
|
259
|
+
version = source_version()
|
|
260
|
+
try:
|
|
261
|
+
result = {
|
|
262
|
+
"version": version,
|
|
263
|
+
"wheel": check_wheel(wheels[0], version),
|
|
264
|
+
"sdist": check_sdist(sdists[0], version),
|
|
265
|
+
}
|
|
266
|
+
except (ValueError, KeyError) as error:
|
|
267
|
+
parser.error(str(error))
|
|
268
|
+
print(json.dumps(result, indent=2))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Fail when the checked-in schema differs from the installed score model."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from audio_as_code import Song
|
|
7
|
+
|
|
8
|
+
path = Path(__file__).resolve().parents[2] / "schemas/song-v1.schema.json"
|
|
9
|
+
if json.loads(path.read_text(encoding="utf-8")) != Song.model_json_schema():
|
|
10
|
+
raise SystemExit("Schema drift: run uv run aac schema -o schemas/song-v1.schema.json")
|
|
11
|
+
print("The checked-in score schema matches Song.model_json_schema().")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Rebuild the sdist and smoke-test both wheels in isolated installed environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from check_distribution import check_wheel, source_version
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def payload(path: Path) -> dict[str, bytes]:
|
|
18
|
+
with zipfile.ZipFile(path) as archive:
|
|
19
|
+
return {
|
|
20
|
+
name: archive.read(name)
|
|
21
|
+
for name in archive.namelist()
|
|
22
|
+
if name.startswith("audio_as_code/")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
28
|
+
parser.add_argument("directory", nargs="?", type=Path, default=Path("dist"))
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
wheels = list(args.directory.glob("*.whl"))
|
|
31
|
+
sdists = list(args.directory.glob("*.tar.gz"))
|
|
32
|
+
if len(wheels) != 1 or len(sdists) != 1:
|
|
33
|
+
parser.error("Expected exactly one wheel and one sdist")
|
|
34
|
+
uv = shutil.which("uv")
|
|
35
|
+
if uv is None:
|
|
36
|
+
parser.error("Install uv to run the isolated distribution checks")
|
|
37
|
+
script = Path(__file__).with_name("smoke_installed.py").resolve()
|
|
38
|
+
with tempfile.TemporaryDirectory(prefix="aac-distribution-") as folder:
|
|
39
|
+
work = Path(folder)
|
|
40
|
+
subprocess.run(
|
|
41
|
+
[
|
|
42
|
+
uv,
|
|
43
|
+
"build",
|
|
44
|
+
str(sdists[0].resolve()),
|
|
45
|
+
"--wheel",
|
|
46
|
+
"--no-sources",
|
|
47
|
+
"--out-dir",
|
|
48
|
+
str(work / "rebuilt"),
|
|
49
|
+
"--python",
|
|
50
|
+
sys.executable,
|
|
51
|
+
],
|
|
52
|
+
check=True,
|
|
53
|
+
cwd=work,
|
|
54
|
+
)
|
|
55
|
+
rebuilt = next((work / "rebuilt").glob("*.whl"))
|
|
56
|
+
check_wheel(rebuilt, source_version())
|
|
57
|
+
assert payload(wheels[0]) == payload(rebuilt), "Sdist rebuild changed the installed package"
|
|
58
|
+
for wheel in (wheels[0].resolve(), rebuilt):
|
|
59
|
+
subprocess.run(
|
|
60
|
+
[
|
|
61
|
+
uv,
|
|
62
|
+
"run",
|
|
63
|
+
"--isolated",
|
|
64
|
+
"--no-project",
|
|
65
|
+
"--python",
|
|
66
|
+
sys.executable,
|
|
67
|
+
"--with",
|
|
68
|
+
str(wheel),
|
|
69
|
+
"python",
|
|
70
|
+
"-I",
|
|
71
|
+
str(script),
|
|
72
|
+
],
|
|
73
|
+
check=True,
|
|
74
|
+
cwd=work,
|
|
75
|
+
)
|
|
76
|
+
print(json.dumps({"sdist_rebuild": "passed", "isolated_wheel_installs": 2}))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Exercise an installed artifact from a temporary working directory, never the checkout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from importlib.metadata import distribution
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import audio_as_code
|
|
13
|
+
from audio_as_code import (
|
|
14
|
+
Note,
|
|
15
|
+
Pattern,
|
|
16
|
+
Song,
|
|
17
|
+
Track,
|
|
18
|
+
analyze_wav,
|
|
19
|
+
export_midi,
|
|
20
|
+
get_instrument,
|
|
21
|
+
inspect_score,
|
|
22
|
+
render,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
repo = Path(__file__).resolve().parents[2]
|
|
28
|
+
package = Path(audio_as_code.__file__).resolve()
|
|
29
|
+
if package.is_relative_to(repo / "src"):
|
|
30
|
+
raise SystemExit("Smoke test imported the checkout; install the built wheel in isolation")
|
|
31
|
+
metadata = distribution("audio-as-code")
|
|
32
|
+
assert metadata.version == audio_as_code.__version__
|
|
33
|
+
assert (package.parent / "py.typed").is_file()
|
|
34
|
+
console = Path(sys.executable).with_name("aac.exe" if sys.platform == "win32" else "aac")
|
|
35
|
+
with tempfile.TemporaryDirectory(prefix="aac-installed-") as folder:
|
|
36
|
+
work = Path(folder)
|
|
37
|
+
for command in ([str(console)], [sys.executable, "-m", "audio_as_code"]):
|
|
38
|
+
version = subprocess.run(
|
|
39
|
+
[*command, "--version"], cwd=work, capture_output=True, text=True, check=True
|
|
40
|
+
)
|
|
41
|
+
assert json.loads(version.stdout) == {"version": metadata.version}
|
|
42
|
+
subprocess.run([*command, "--help"], cwd=work, capture_output=True, check=True)
|
|
43
|
+
phrase = Pattern.sequence(["C4"], step=0.25)
|
|
44
|
+
phrase = phrase.overlay(phrase.transpose(12), offset=0.25).stretch(2).scale_velocity(0.5)
|
|
45
|
+
song = Song(
|
|
46
|
+
title="Installed artifact smoke test",
|
|
47
|
+
bpm=120,
|
|
48
|
+
beats=1,
|
|
49
|
+
sample_rate=22050,
|
|
50
|
+
seed=7,
|
|
51
|
+
tracks=[Track(name="Lead", instrument="sine", notes=phrase.notes)],
|
|
52
|
+
)
|
|
53
|
+
score = work / "score.json"
|
|
54
|
+
song.save(score)
|
|
55
|
+
assert Song.load(score) == song
|
|
56
|
+
inspection = inspect_score(song)
|
|
57
|
+
assert inspection["summary"]["note_count"] == 2
|
|
58
|
+
assert inspection["readiness"]["render"]["ready"]
|
|
59
|
+
assert inspection["readiness"]["midi"]["ready"]
|
|
60
|
+
inspected = subprocess.run(
|
|
61
|
+
[str(console), "inspect", str(score)],
|
|
62
|
+
cwd=work,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
check=True,
|
|
66
|
+
)
|
|
67
|
+
assert json.loads(inspected.stdout) == inspection
|
|
68
|
+
report = render(song, work / "score.wav")
|
|
69
|
+
export_midi(song, work / "score.mid")
|
|
70
|
+
assert report["wav"] == analyze_wav(work / "score.wav")
|
|
71
|
+
assert not report["wav"]["silent"] and not report["wav"]["full_scale_samples"]
|
|
72
|
+
validation = subprocess.run(
|
|
73
|
+
[str(console), "validate", str(score)],
|
|
74
|
+
cwd=work,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
check=True,
|
|
78
|
+
)
|
|
79
|
+
assert json.loads(validation.stdout)["valid"]
|
|
80
|
+
for voice in ("mandolin", "kalimba", "celesta", "recorder"):
|
|
81
|
+
info = get_instrument(voice)
|
|
82
|
+
probe = Song(
|
|
83
|
+
bpm=120,
|
|
84
|
+
beats=1,
|
|
85
|
+
sample_rate=22050,
|
|
86
|
+
tracks=[
|
|
87
|
+
Track(
|
|
88
|
+
name=voice,
|
|
89
|
+
instrument=voice,
|
|
90
|
+
notes=[Note(pitch=info.preview_pitch, duration=1)],
|
|
91
|
+
)
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
voice_report = render(probe, work / f"{voice}.wav")
|
|
95
|
+
assert not voice_report["wav"]["silent"]
|
|
96
|
+
assert not voice_report["wav"]["full_scale_samples"]
|
|
97
|
+
export_midi(probe, work / f"{voice}.mid")
|
|
98
|
+
invalid = work / "invalid.json"
|
|
99
|
+
invalid.write_text('{"tracks":[]}', encoding="utf-8")
|
|
100
|
+
failure = subprocess.run(
|
|
101
|
+
[str(console), "validate", str(invalid)],
|
|
102
|
+
cwd=work,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
)
|
|
106
|
+
assert failure.returncode == 2 and not failure.stdout
|
|
107
|
+
assert json.loads(failure.stderr)["error"] == "invalid_score"
|
|
108
|
+
print(json.dumps({"version": metadata.version, "installed_artifact_smoke": "passed"}))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request, workflow_dispatch]
|
|
3
|
+
|
|
4
|
+
permissions:
|
|
5
|
+
contents: read
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
hosting:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
timeout-minutes: 5
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: '24'
|
|
22
|
+
- run: npm ci
|
|
23
|
+
working-directory: web/cloudflare
|
|
24
|
+
- run: npm exec --no -- wrangler --version
|
|
25
|
+
working-directory: web/cloudflare
|
|
26
|
+
- run: node --test web/cloudflare/media-handler.test.js
|
|
27
|
+
|
|
28
|
+
test:
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
os: [ubuntu-latest, windows-latest]
|
|
33
|
+
python: ['3.10', '3.13']
|
|
34
|
+
include:
|
|
35
|
+
- os: macos-latest
|
|
36
|
+
python: '3.13'
|
|
37
|
+
runs-on: ${{ matrix.os }}
|
|
38
|
+
timeout-minutes: 20
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
41
|
+
with:
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
44
|
+
with:
|
|
45
|
+
python-version: ${{ matrix.python }}
|
|
46
|
+
version: '0.12.15'
|
|
47
|
+
- run: uv sync --locked
|
|
48
|
+
- name: Schema stays in sync with the score model
|
|
49
|
+
run: uv run --locked python .github/scripts/check_schema.py
|
|
50
|
+
- run: uv run --locked pytest
|
|
51
|
+
|
|
52
|
+
distribution:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
timeout-minutes: 15
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
57
|
+
with:
|
|
58
|
+
persist-credentials: false
|
|
59
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
60
|
+
with:
|
|
61
|
+
python-version: '3.13'
|
|
62
|
+
version: '0.12.15'
|
|
63
|
+
- run: uv sync --locked
|
|
64
|
+
- run: uv run --locked ruff check .
|
|
65
|
+
- run: uv run --locked ruff format --check .
|
|
66
|
+
- name: Build without local dependency source overrides
|
|
67
|
+
run: uv build --no-sources
|
|
68
|
+
- name: Inspect archive contents and metadata
|
|
69
|
+
run: uv run --locked python .github/scripts/check_distribution.py dist
|
|
70
|
+
- name: Install wheel and rebuilt sdist outside the checkout
|
|
71
|
+
run: uv run --locked python .github/scripts/smoke_distribution.py dist
|