diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index c3ae2a4..928cac7 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -18,6 +18,13 @@ jobs: with: node-version: '22' + - uses: jdx/mise-action@v2 + + - name: Validate homepage query against parsed graph + run: | + npm ci --no-audit --no-fund + mise run site:query:check + - name: Scan published source for secrets and local paths run: | npm install --prefix "$RUNNER_TEMP/secretlint" --no-audit --no-fund secretlint@13.0.6 @secretlint/secretlint-rule-preset-recommend@13.0.6 @secretlint/secretlint-rule-pattern@13.0.6 @@ -26,15 +33,24 @@ jobs: - uses: astral-sh/setup-uv@v6 - name: Check podcast RSS - run: uv run --no-project --with PyYAML==6.0.2 python scripts/build_rss.py --check + run: uv run --no-project python scripts/build_rss.py --check + + - name: Prepare episode and asset visibility + run: uv run --no-project python scripts/prepare_site.py --output "$RUNNER_TEMP/gitp-site" - name: Logseq Publish uses: logseq/publish-spa@main with: - graph-directory: gitp-garden + graph-directory: ${{ runner.temp }}/gitp-site output-directory: www accent-color: indigo + - name: Save website preview + uses: actions/upload-artifact@v4 + with: + name: gitpa-website + path: www + - name: Add .nojekyll file run: touch www/.nojekyll diff --git a/.gitignore b/.gitignore index d6848dc..494fb10 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ Thumbs.db __pycache__/ *.py[cod] .pytest_cache/ + +node_modules/ +.nbb/ diff --git a/.secretlintignore b/.secretlintignore index 5d62b5b..1487615 100644 --- a/.secretlintignore +++ b/.secretlintignore @@ -4,3 +4,5 @@ node_modules/ **/*.mid **/*.mfpz **/*.mp3 + +.nbb/ diff --git a/README.md b/README.md index b9f9ab7..7c28778 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ -# gitpa -Ghost in the Patch Alpha podcast +# Ghost in the Patch -## Podcast feed +Episode notes and media links live in [logseq-encode-garden](https://github.com/codekiln/logseq-encode-garden). Gitpa mirrors each session under its exact page name, together with its embedded audio asset page. The website and RSS read those checked-in pages. -Published episodes have an `episode.yml` under `gitp-garden/assets/Ceremony////` and a public page under `gitp-garden/pages/`. Set `published: true` only when the episode record has its permanent GUID, publication time, public page URL, and verified MP3 enclosure URL and length. Existing records without `published: true` remain drafts. +To mirror or refresh an episode, run the garden's sync task, imported into `gitp-garden/mise-tasks/`: `mise run logseq:entity:proxy:page:sync --source /path/to/logseq-encode-garden --destination gitp-garden --page 'GitP/A/Session/26/09/24-Thu' --follow-embeds --apply`. `--follow-embeds` brings the recording, artwork, preset and MIDI pages the session embeds in the same batch; leave out `--apply` to preview. The page bodies are replaced with the garden's. Properties follow the garden too, except `tags::`, `public::`, `podcast-guid::` and `podcast-published-at::`, which stay as Gitpa set them; the preview lists each property it adds, updates or removes. A destination page without a matching proxy URL stops the batch before writing. The task also brings each page's entity definitions and records what it imported in `gitp-garden/.logseq-proxy/manifest.json`. Re-running it against unchanged garden pages changes nothing except `logseq-proxy-last-sync-date::`. -Run `mise run rss:build` after editing an episode record and commit the resulting `rss.xml`. `mise run rss:check` verifies that the committed feed matches the records. The Pages workflow checks the feed and copies it to `https://codekiln.github.io/gitpa/rss.xml` after building the Logseq site. +For dependent PRs, pass the upstream worktree path explicitly with `--source`. Proxy graph links keep the registered `logseq-encode-garden` name; source links identify the worktree branch. After the upstream PR merges, re-sync from the registered checkout to update the source links to `main`. -Production notes, Ableton inspection, transcription and media preparation live in [logseq-encode-garden](https://github.com/codekiln/logseq-encode-garden). The garden's `gitpa:episode:draft` task prepares a handoff JSON file; import that file with `mise run episode:import -- '/path/to/episode-handoff.json'`. +To update the imported sync task itself, sync its entity definition: `mise run logseq:entity:proxy:page:sync --source /path/to/logseq-encode-garden --destination gitp-garden --page 'Logseq/Entity/Proxy/Page' --apply`. -The handoff contains `recorded_on` (YYYY-MM-DD), `episode_title` and `description`. Optional verified media metadata must include `audio_url`, `audio_length` and `audio_type` together. The importer creates an unpublished `episode.yml` and a Logseq draft page, preserves existing editorial copy, page content and publication identity, and rejects conflicting enclosure metadata before writing. Repeated imports leave existing copy untouched. Unknown fields, including `published`, `guid` and `page`, are rejected; `gitpa` owns the final copy, page, GUID and publication time. Review the description and verify the public MP3 before setting `published: true`. +Set `public:: true` on the episode page to release the episode on the website and RSS. The synced episode body contains direct artwork, recording and preset download links. The desktop graph hides administrative properties and displays `logseq-entity` and `date-created`. The website build also resolves older recording and artwork embeds into direct links. Proxy and storage properties remain in the checked-in garden; the exported listener view hides page properties. A `Notes` or `Production notes` heading publishes its children only when the heading carries `public:: true`. The website build derives asset-page visibility from public episodes; shared assets stay visible while any public episode links to them. The episode's `podcast-guid::` identifies its RSS item permanently; `podcast-published-at::` holds its publication time with a timezone offset. The RSS builder reads the title and description from the session's opening heading and paragraph, reads its direct MP3 recording link (or an older embedded MP3 asset page), and gets the enclosure size from the public MP3 server. `public:: false` keeps an episode out of the website and RSS. -Install the staged-file guard with `lefthook install`. It uses the machine's `mise run secrets:scan` task, including private identity patterns. GitHub Actions runs the checked-in Secretlint rules across the public source before publishing. +The November and December 2024 recordings enter the feed with this release. Their publication times record that release; their recording dates remain on the session pages. The September 2026 episodes retain their existing feed identities and publication times. + +Run `mise run rss:build` and commit `rss.xml` with a publication PR. `mise run rss:check` checks the feed against the public pages and audio server; `mise run rss:test` runs offline tests. Merging a publication PR triggers the Pages workflow, which publishes the website and [podcast feed](https://codekiln.github.io/gitpa/rss.xml). + +Install the staged-file guard with `lefthook install`. It uses the machine's `mise run secrets:scan` task. GitHub Actions scans the public source before publishing. + +The homepage lists published episodes by publication time, newest first. Preparation normalizes publication timestamps to UTC in the temporary website graph so different timezone offsets sort chronologically. Run `mise install` and `npm ci`, then `mise run site:query:check` to test the actual query, result transform and view using the Logseq file parser. Before running the query, the check rewrites `(pull ?b [*])` the way the Logseq app does, which drops page names; the homepage query therefore pulls `?p`. The check includes false and string-valued publication fixtures. PR builds upload a `gitpa-website` artifact for browser review. diff --git a/gitp-acolyte/.gitignore b/gitp-acolyte/.gitignore deleted file mode 100644 index fabd87d..0000000 --- a/gitp-acolyte/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Python specific ignores -__pycache__/ -*.py[cod] -*.so -*.egg -*.egg-info/ -dist/ -build/ -*.pyo -*.pyd - -# Virtual environment -venv/ -ENV/ -env/ -.venv/ -.ENV/ -.env/ - -# Jupyter Notebook -.ipynb_checkpoints - -# PyCharm specific ignores -.idea/ - -# VS Code specific ignores -.vscode/ - -# macOS specific ignores -.DS_Store - -# Ignore .env file -.env \ No newline at end of file diff --git a/gitp-acolyte/.python-version b/gitp-acolyte/.python-version deleted file mode 100644 index 819f443..0000000 --- a/gitp-acolyte/.python-version +++ /dev/null @@ -1 +0,0 @@ -gitp-acolyte diff --git a/gitp-acolyte/Makefile b/gitp-acolyte/Makefile deleted file mode 100644 index 457abce..0000000 --- a/gitp-acolyte/Makefile +++ /dev/null @@ -1,97 +0,0 @@ -.PHONY: pyclean create-ep-page create-ep-data create-ep-data-force test-openai - -export PYTHONDONTWRITEBYTECODE=1 - -pyclean: - @echo "Cleaning up __pycache__ directories and .pyc, .pyo, .pyd files..." - @pycache_count=$$(find . -type d -name "__pycache__" | wc -l); \ - if [ $$pycache_count -eq 0 ]; then \ - echo "No __pycache__ directories found"; \ - else \ - echo "Found $$pycache_count __pycache__ directories"; \ - find . -type d -name "__pycache__" -exec rm -r {} +; \ - fi - @pyc_count=$$(find . -type f -name "*.py[cod]" | wc -l); \ - if [ $$pyc_count -eq 0 ]; then \ - echo "No .pyc, .pyo, .pyd files found"; \ - else \ - echo "Found $$pyc_count .pyc, .pyo, .pyd files"; \ - find . -type f -name "*.py[cod]" -exec rm -f {} +; \ - fi - @echo "Cleanup complete." - - -# create-ep-page: Generates an episode page for the given date. -# Usage: -# epdate=2024-12-05 make create-ep-page -create-ep-page: - @if [ -z "$(epdate)" ]; then \ - echo "Error: epdate is not set. Usage: epdate=YYYY-MM-DD make create-ep-page"; \ - exit 1; \ - fi - poetry run python -m gitp_acolyte.ceremonial.spells.episode_page.create_episode_page $(epdate) - - -# create-ref-ep-page: Generates an episode page for the given date. -# Usage: -# make create-ref-ep-page -create-ref-ep-page: - poetry run python -m gitp_acolyte.ceremonial.spells.episode_page.create_episode_page --reference - -# create-ep-page-force: Generates an episode page for the given date, forcing the creation of the page. -# Usage: -# epdate=2024-12-05 make create-ep-page -create-ep-page-force: - @if [ -z "$(epdate)" ]; then \ - echo "Error: epdate is not set. Usage: epdate=YYYY-MM-DD make create-ep-page"; \ - exit 1; \ - fi - poetry run python -m gitp_acolyte.ceremonial.spells.episode_page.create_episode_page $(epdate) --force - -# create-ep-data: Generates episode data for the given date. -# Usage: -# epdate=2024-12-05 make create-ep-data -create-ep-data: - @if [ -z "$(epdate)" ]; then \ - echo "Error: epdate is not set. Usage: epdate=YYYY-MM-DD make create-ep-data"; \ - exit 1; \ - fi - poetry run python -m gitp_acolyte.ceremonial.spells.episode_data.create $(epdate) - -create-ep-data-reference: - poetry run python -m gitp_acolyte.ceremonial.spells.episode_data.create --reference - -# create-ep-data-ref-recreate: Generates episode data for the reference date, recreating the episode.yml file if it already exists. -# Usage: -# make create-ep-data-ref-recreate -create-ep-data-ref-recreate: - poetry run python -m gitp_acolyte.ceremonial.spells.episode_data.create --reference --recreate - -# update-ep-data-file-attrs-reference: Updates the file attributes for the reference episode. -# Usage: -# make update-ep-data-file-attrs-reference -update-ep-data-file-attrs-reference: - poetry run python -m gitp_acolyte.ceremonial.spells.episode_data.update.file_attrs.update_file_attrs --reference - -# update-ep-data-ref-recreate: Updates the file attributes for the reference episode, recreating the episode.yml file if it already exists. -# Usage: -# make update-ep-data-ref-recreate -update-ep-data-ref-recreate: - poetry run python -m gitp_acolyte.ceremonial.spells.episode_data.update.file_attrs.update_file_attrs --reference --recreate - -# update-ep-data-file-attrs: Updates the file attributes for the given date. -# Usage: -# epdate=2024-11-19 make update-ep-data-file-attrs -update-ep-data-file-attrs: - @if [ -z "$(epdate)" ]; then \ - echo "Error: epdate is not set. Usage: epdate=YYYY-MM-DD make update-ep-data-file-attrs"; \ - exit 1; \ - fi - poetry run python -m gitp_acolyte.ceremonial.spells.episode_data.update.file_attrs.update_file_attrs $(epdate) - -# test-openai: Tests connectivity to the OpenAI API. -# Usage: -# make test-openai -test-openai: - poetry run python -m gitp_acolyte.utils.ai.test_openai - diff --git a/gitp-acolyte/README.md b/gitp-acolyte/README.md deleted file mode 100644 index 6c4cd8d..0000000 --- a/gitp-acolyte/README.md +++ /dev/null @@ -1 +0,0 @@ -# gitp-acolyte diff --git a/gitp-acolyte/gitp_acolyte/__init__.py b/gitp-acolyte/gitp_acolyte/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/offering/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/offering/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/args.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/args.py deleted file mode 100644 index 7b427c8..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/args.py +++ /dev/null @@ -1,69 +0,0 @@ -import argparse -from datetime import datetime -from pathlib import Path -from gitp_acolyte.constants import DATE_FORMAT, REFERENCE_EPISODE_DIR, get_relative_path -from gitp_acolyte.ceremonial.spells.episode_reference.constants import DEFAULT_REFERENCE_EPISODE_YML_PATH, REFERENCE_EPISODE_DATE - - -def define_common_reference_file_args(parser: argparse.ArgumentParser | None = None) -> argparse.ArgumentParser: - """ - Defines common argparse arguments for operating on reference versions of the files. - """ - parser.add_argument( - '--reference', - action='store_true', - help='Use the reference episode. Used for testing the script.' - ) - - ref_ep_dir_relpath = str(get_relative_path(REFERENCE_EPISODE_DIR)) - ref_ep_dir_help = f'Path to the reference episode directory. Default is {ref_ep_dir_relpath}.' - parser.add_argument( - '--reference-dir', - type=Path, - default=REFERENCE_EPISODE_DIR, - help=ref_ep_dir_help - ) - - ref_ep_yml_relpath = str(get_relative_path(DEFAULT_REFERENCE_EPISODE_YML_PATH)) - ref_ep_yml_help = f'Use the reference episode YAML file. Default is "{ref_ep_yml_relpath}".' - parser.add_argument( - '--use-ref-ep-yml', - action='store_true', - help=ref_ep_yml_help, - default=False - ) - - return parser - - -def define_common_args(parser: argparse.ArgumentParser | None = None) -> argparse.ArgumentParser: - """ - Defines common argparse arguments. - The default date should be today. - """ - if parser is None: - parser = argparse.ArgumentParser(description="Process a date.") - - parser.add_argument( - 'date', - type=lambda s: datetime.strptime(s, DATE_FORMAT), - default=datetime.today().strftime(DATE_FORMAT), - nargs='?', - help=f'Date in {{DATE_FORMAT}} format. Default is today.' - ) - define_common_reference_file_args(parser) - - parser.add_argument( - '--recreate', - action='store_true', - help='Recreate the episode.yml file if it already exists.' - ) - return parser - -def get_episode_date(args): - """ - Returns the episode date based on the provided arguments. - """ - if args.reference: - return REFERENCE_EPISODE_DATE - return args.date diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/create.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/create.py deleted file mode 100644 index 47d4684..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/create.py +++ /dev/null @@ -1,250 +0,0 @@ -import argparse -import logging -from pathlib import Path - -import coloredlogs -import yaml - -from gitp_acolyte.ceremonial.spells.episode_data.args import ( - define_common_args, - get_episode_date, -) -from gitp_acolyte.constants import ( - DATE_FORMAT, - EPISODE_YAML_FILENAME, - LOGSEQ_ASSETS_FOLDER, - LOGSEQ_FOLDER, - RECORDINGS_ROOT_FOLDER, - REFERENCE_EPISODE_DIR, - REPO_ROOT, - SHORT_DATE_FORMAT, -) - -# Configure logging -logger = logging.getLogger(__name__) -coloredlogs.install( - level="DEBUG", - logger=logger, - fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) - - -def validate_directories(): - directories = [ - RECORDINGS_ROOT_FOLDER.expanduser(), - LOGSEQ_FOLDER, - LOGSEQ_ASSETS_FOLDER, - ] - - for directory in directories: - if directory.exists() and directory.is_dir(): - # logger.debug(f"Directory {directory} exists and is accessible.") - pass - else: - logger.warning( - f"Directory {directory} does not exist or is not accessible." - ) - - -def define_args(): - """ - Defines argparse arguments. - """ - parser = argparse.ArgumentParser(description="Create Episode Data File.") - parser = define_common_args() - return parser.parse_args() - - -def construct_episode_recording_dir_name(episode_date): - """ - Constructs the episode recording directory name. - Should handle the date format so the directories are like "GitP.24.12.04 Project" - """ - episode_recording_dir_name = ( - f"GitP.{episode_date.strftime(SHORT_DATE_FORMAT)} Project" - ) - return episode_recording_dir_name - - -def construct_path_to_episode_recording_dir(episode_date): - """ - Constructs the path to the episode recording directory. - """ - episode_recording_dir_name = construct_episode_recording_dir_name(episode_date) - episode_recording_dir = RECORDINGS_ROOT_FOLDER / episode_recording_dir_name - return episode_recording_dir.expanduser() - - -def episode_recording_dir_exists(episode_date): - """ - Checks if the episode recording directory exists. - """ - path_to_episode_recording_dir = construct_path_to_episode_recording_dir( - episode_date - ) - return path_to_episode_recording_dir.exists() - - -def construct_path_to_episode_publishing_dir(episode_date, args): - """ - Constructs the path to the episode publishing directory. - """ - if args.reference: - ref_ep_dir = REFERENCE_EPISODE_DIR - expected_ref_ep_dir = ( - REPO_ROOT - / "gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir" - ) - assert ( - ref_ep_dir == expected_ref_ep_dir - ), f"Reference episode directory is not as expected: \nactual: '{ref_ep_dir}' \nexpected: '{expected_ref_ep_dir}'" - return REFERENCE_EPISODE_DIR - year = episode_date.strftime("%Y") - month = episode_date.strftime("%m") - day = episode_date.strftime("%d") - episode_publishing_dir = LOGSEQ_ASSETS_FOLDER / "Ceremony" / year / month / day - return episode_publishing_dir - - -def ensure_episode_publishing_dir(episode_date, args): - """ - Ensures the episode publishing directory exists. - Returns True if the directory was created, False if it already existed. - """ - path_to_episode_publishing_dir = construct_path_to_episode_publishing_dir( - episode_date, args - ) - if not path_to_episode_publishing_dir.exists(): - path_to_episode_publishing_dir.mkdir(parents=True, exist_ok=True) - logger.debug( - f"Episode publishing directory created: {path_to_episode_publishing_dir}" - ) - return True - logger.debug( - f"Episode publishing directory already exists: {path_to_episode_publishing_dir}" - ) - return False - - -def path_to_episode_publishing_yml(episode_date, args): - """ - Constructs the path to the episode.yml file in the episode publishing directory. - """ - path_to_episode_publishing_dir = construct_path_to_episode_publishing_dir( - episode_date, args - ) - return path_to_episode_publishing_dir / EPISODE_YAML_FILENAME - - -def ensure_episode_yaml(episode_date, args): - """ - Ensures that episode.yml exists in the episode publishing directory - """ - episode_yaml_path = path_to_episode_publishing_yml(episode_date, args) - - if episode_yaml_path.exists() and args.recreate: - episode_yaml_path.unlink() - logger.info( - f"--recreate: Removed existing episode.yml file: {episode_yaml_path}" - ) - - if not episode_yaml_path.exists(): - episode_data = {"episode_date": episode_date.strftime(DATE_FORMAT)} - with episode_yaml_path.open("w") as file: - yaml.dump(episode_data, file) - return True - else: - with episode_yaml_path.open("r") as file: - episode_data = yaml.safe_load(file) - - if "episode_date" not in episode_data or episode_data[ - "episode_date" - ] != episode_date.strftime(DATE_FORMAT): - episode_data["episode_date"] = episode_date.strftime(DATE_FORMAT) - with episode_yaml_path.open("w") as file: - yaml.dump(episode_data, file) - return True - return False - - -def recording_dir_exists(episode_date, args) -> tuple[Path, bool]: - """ - Checks if the episode recording directory exists. - Returns a tuple of: - - the path to the episode recording directory - - a boolean indicating if the directory exists - """ - path_to_episode_recording_dir = construct_path_to_episode_recording_dir( - episode_date - ) - logger.debug( - f"Path to episode recording directory: '{path_to_episode_recording_dir}'" - ) - if not path_to_episode_recording_dir.exists(): - if args.reference: - logger.info( - "Will not create reference episode recording directory. " - f"It would have been at {path_to_episode_recording_dir}. " - ) - else: - logger.error( - f"Episode recording directory does not exist: {path_to_episode_recording_dir}" - ) - logger.error("Please create the episode recording directory.") - return path_to_episode_recording_dir, False - return path_to_episode_recording_dir, True - - -def episode_publishing_dir_exists(episode_date, args) -> tuple[Path, bool]: - path_to_episode_publishing_dir = construct_path_to_episode_publishing_dir( - episode_date, args - ) - logger.debug( - f"Path to episode publishing directory: {path_to_episode_publishing_dir}" - ) - if not path_to_episode_publishing_dir.exists(): - if args.reference: - logger.info("Reference episode directory does not exist. ") - else: - logger.debug( - f"Episode publishing directory does not exist: {path_to_episode_publishing_dir}" - ) - return path_to_episode_publishing_dir, False - return path_to_episode_publishing_dir, True - - -def ensure_episode_dir_and_yaml_exists(episode_date, args) -> tuple[Path, Path]: - """ - Ensures the episode publishing directory and episode.yml file exist. - Returns a tuple of: - - the path to the episode publishing directory - - the path to the episode.yml file - """ - ep_pub_dir, ep_pub_dir_exists = episode_publishing_dir_exists(episode_date, args) - - if not ep_pub_dir_exists: - episode_publishing_dir_created = ensure_episode_publishing_dir( - episode_date, args - ) - - episode_yaml_created = ensure_episode_yaml(episode_date, args) - logger.debug(f"Episode YAML created or updated: {episode_yaml_created}") - return ep_pub_dir, path_to_episode_publishing_yml(episode_date, args) - - -def main(): - validate_directories() - args = define_args() - episode_date = get_episode_date(args) - logger.debug(f"Episode date: {episode_date}") - - # check the episode recording dir - # we're not doing any info with this for now - # eventually, maybe call a script to create the recording dir - recording_dir_exists(episode_date, args) - - ensure_episode_dir_and_yaml_exists(episode_date, args) - - -if __name__ == "__main__": - main() diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs.py deleted file mode 100644 index 07fbad6..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -update_file_attrs.py - -Given a path to a path to the logseq directory for an episode, -update the attributes in episode.yml that point to files in that directory. - -Uses AI to infer the files. - -Usage: - update_file_attrs.py - update the file attributes for the given episode date. - update_file_attrs.py --reference - update the file attributes for the reference episode. -""" - -import argparse -import json -import logging -from datetime import datetime -from pathlib import Path - -import coloredlogs -import yaml -from dotenv import load_dotenv -from openai import OpenAI -from openai.types.chat.parsed_chat_completion import ParsedChatCompletion - -from gitp_acolyte.ceremonial.spells.episode_data.args import ( - define_common_args, - get_episode_date, -) -from gitp_acolyte.ceremonial.spells.episode_data.create import ( - ensure_episode_dir_and_yaml_exists, -) -from gitp_acolyte.ceremonial.spells.episode_reference.episode_schema import ( - PodcastEpisodePublicationData, -) -from gitp_acolyte.constants import ( - EPISODE_YAML_FILENAME, - REFERENCE_EPISODE_DIR, - get_relative_path, -) - -# Configure logging -logger = logging.getLogger(__name__) -coloredlogs.install( - level="DEBUG", - logger=logger, - fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) - - -def get_args(): - parser = argparse.ArgumentParser( - description="Update Episode yaml data attributes, inferring from the filesystem." - ) - parser = define_common_args() - return parser.parse_args() - - -def get_system_prompt() -> str: - """ - Load the system prompt from update_file_attrs_system_prompt.md - """ - script_dir = Path(__file__).parent - prompt_path = script_dir / "update_file_attrs_system_prompt.md" - logger.debug(f"Loading system prompt from {get_relative_path(prompt_path)}") - with prompt_path.open() as f: - return f.read() - - -def get_user_prompt(podcast_directory_info: str) -> str: - """ - Load the user prompt from update_file_attrs_user_prompt.md - """ - script_dir = Path(__file__).parent - prompt_path = script_dir / "update_file_attrs_user_prompt.md" - logger.debug(f"Loading user prompt from {get_relative_path(prompt_path)}") - with prompt_path.open() as f: - prompt = f.read() - return prompt.format(podcast_directory_info=podcast_directory_info) - - -def call_openai( - podcast_directory_info: str, -) -> ParsedChatCompletion[PodcastEpisodePublicationData]: - load_dotenv() - client = OpenAI() - - system_prompt = get_system_prompt() - user_prompt = get_user_prompt(podcast_directory_info) - - logger.debug("Calling OpenAI ...") - podcast_episode_publication_data: PodcastEpisodePublicationData = ( - client.beta.chat.completions.parse( - messages=[ - { - "role": "system", - "content": system_prompt, - }, - { - "role": "user", - "content": user_prompt, - }, - ], - model="gpt-4o-mini", - response_format=PodcastEpisodePublicationData, - ) - ) - # podcast_ep_info = podcast_episode_publication_data.choices[0].message.model_dump()['parsed'] - return podcast_episode_publication_data - - -def get_episode_dir_ai_info(pathlib_dir_obj: Path) -> dict: - """ - Get directory information similar to `ls -la`. - Returns a dictionary with directory name, and for each file, a file name, - the size of the file, the created and last modified date in a standard ISO time format. - """ - dir_info = {"directory_name": pathlib_dir_obj.name, "files": []} - for file in pathlib_dir_obj.iterdir(): - if file.is_file(): - file_info = { - "file_name": file.name, - "size": file.stat().st_size, - "created": datetime.fromtimestamp(file.stat().st_ctime).isoformat(), - "last_modified": datetime.fromtimestamp( - file.stat().st_mtime - ).isoformat(), - } - dir_info["files"].append(file_info) - return dir_info - - -def serialize_episode_dir_ai_info(dir_info: dict) -> str: - """ - Serialize the directory information to a string. - """ - serialized_episode_directory_info = json.dumps(dir_info, indent=4) - logger.debug(f"{serialized_episode_directory_info=}") - return serialized_episode_directory_info - - -def update_file_attrs(episode_dir, args): - dir_info = get_episode_dir_ai_info(episode_dir) - serialized_dir_info = serialize_episode_dir_ai_info(dir_info) - podcast_episode_publication_data: ParsedChatCompletion[ - PodcastEpisodePublicationData - ] = call_openai(serialized_dir_info) - write_episode_yaml(podcast_episode_publication_data, episode_dir, args) - - -def write_episode_yaml( - podcast_episode_publication_data: ParsedChatCompletion[ - PodcastEpisodePublicationData - ], - episode_dir: Path, - args, -): - """ - Write the episode data to the episode.yml file. - If args.reference is True, write to the reference episode directory. - """ - if args.reference: - yaml_path = REFERENCE_EPISODE_DIR / EPISODE_YAML_FILENAME - else: - yaml_path = episode_dir / EPISODE_YAML_FILENAME - - with yaml_path.open("w") as f: - pydantic_serialized_to_yaml_string = serialize_pydantic_to_yaml( - podcast_episode_publication_data - ) - f.write(pydantic_serialized_to_yaml_string) - - logger.debug(f"Written episode data to {get_relative_path(yaml_path)}") - - -def serialize_pydantic_to_yaml( - podcast_episode_publication_data: ParsedChatCompletion[ - PodcastEpisodePublicationData - ], -) -> str: - """ - Serialize the PodcastEpisodePublicationData to a YAML string. - """ - data_dict = podcast_episode_publication_data.model_dump() - data_dict_str = json.dumps(data_dict, indent=4) - logger.debug(f"podcast_episode_publication_data: {data_dict_str}") - - parsed_response_json = podcast_episode_publication_data.choices[0].message.content - episode_dict = json.loads(parsed_response_json) - yaml_string = yaml.dump(episode_dict, sort_keys=True, default_flow_style=False) - return yaml_string - - -def main(): - args = get_args() - episode_date = get_episode_date(args) - logger.debug(f"Episode date: {episode_date}") - episode_dir, _ = ensure_episode_dir_and_yaml_exists(episode_date, args) - logger.debug(f"Episode directory: {get_relative_path(episode_dir)}") - update_file_attrs(episode_dir, args) - logger.info("File attributes updated successfully.") - - -if __name__ == "__main__": - main() diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs_system_prompt.md b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs_system_prompt.md deleted file mode 100644 index 44b385d..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs_system_prompt.md +++ /dev/null @@ -1,3 +0,0 @@ -- You are a production assistant creating the Episode Data for a single podcast episode. -- You are given PODCAST_DIRECTORY_INFO information which describes the episode's file system directory. -- **Your job** is to construct the Episode Data using the PODCAST_DIRECTORY_INFO information. \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs_user_prompt.md b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs_user_prompt.md deleted file mode 100644 index 03ec8c2..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_data/update/file_attrs/update_file_attrs_user_prompt.md +++ /dev/null @@ -1,5 +0,0 @@ -Here's the current episode directory info: - -{podcast_directory_info} - -Please populate episode data. \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/create_episode_page.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/create_episode_page.py deleted file mode 100644 index f936e76..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/create_episode_page.py +++ /dev/null @@ -1,132 +0,0 @@ -import argparse -import logging -import os -from datetime import datetime - -import coloredlogs -import yaml -from jinja2 import Environment, FileSystemLoader - -from gitp_acolyte.ceremonial.spells.episode_data.args import ( - define_common_args, - get_episode_date, -) -from gitp_acolyte.ceremonial.spells.episode_data.create import ( - path_to_episode_publishing_yml, -) -from gitp_acolyte.ceremonial.spells.episode_reference.constants import ( - DEFAULT_REFERENCE_EPISODE_YML_PATH, - DRAFT_REFERENCE_EPISODE_OUTPUT_PATH, - REFERENCE_EPISODE_DATE, -) -from gitp_acolyte.constants import ( - EPISODE_YAML_FILENAME, - GITHUB_USER_CONTENT_ASSETS_BASE_URL, - LOGSEQ_PAGES_FOLDER, - get_relative_path, -) - -# Configure logging -logger = logging.getLogger(__name__) -coloredlogs.install( - level="DEBUG", - logger=logger, - fmt="%(asctime)s - %(module)s - %(levelname)s - %(message)s", -) - - -def get_argparse_args(): - parser = argparse.ArgumentParser(description="Generate episode page.") - define_common_args(parser) # Use common argparse arguments - parser.add_argument( - "--force", - action="store_true", - help="Force overwrite if the episode page already exists.", - ) - return parser.parse_args() - - -def get_episode_page_filename(episode_date): - """ - Constructs the episode page filename. - """ - return f"Ceremony___{episode_date.strftime('%Y')}___{episode_date.strftime('%m')}___{episode_date.strftime('%d')}.md" - - -def get_episode_page_path(episode_date): - """ - Constructs the full path to the episode page. - """ - filename = get_episode_page_filename(episode_date) - return LOGSEQ_PAGES_FOLDER / filename - - -def load_episode_yaml_and_ensure_context(episode_yaml_path): - """ - Loads the episode YAML data from the given path. - """ - with open(episode_yaml_path, "r") as f: - data = yaml.safe_load(f) - # Derive date_created from episode_date to avoid duplication - if "episode_date" in data: - episode_dt = datetime.strptime(data["episode_date"], "%Y-%m-%d") - data["date_created"] = episode_dt.strftime("%Y-%m-%d %a") - else: - logger.error("episode_date not found in YAML.") - exit(1) - - # Add assets_base_url to the context - data["assets_base_url"] = GITHUB_USER_CONTENT_ASSETS_BASE_URL - - logger.debug( - f"Loaded episode YAML for date {episode_dt} from {episode_yaml_path}" - ) - logger.debug(f"Episode Data: {data}") - return data - - -def main(): - args = get_argparse_args() - script_dir = os.path.dirname(os.path.abspath(__file__)) - - if args.reference: - output_file = DRAFT_REFERENCE_EPISODE_OUTPUT_PATH - episode_date = REFERENCE_EPISODE_DATE - episode_yaml_path = args.reference_dir / EPISODE_YAML_FILENAME - if args.use_ref_ep_yml: - episode_yaml_path = DEFAULT_REFERENCE_EPISODE_YML_PATH - else: - episode_date = get_episode_date(args) - output_file = get_episode_page_path(episode_date) - - if output_file.exists() and not args.force: - logger.warning( - f"Episode page {output_file} already exists. Use --force to overwrite." - ) - exit(1) - - episode_yaml_path = path_to_episode_publishing_yml(episode_date, args) - if not episode_yaml_path.exists(): - logger.warning(f"Episode YAML {episode_yaml_path} does not exist.") - exit(1) - - # Load episode YAML - data = load_episode_yaml_and_ensure_context(episode_yaml_path) - - # Setup Jinja environment - env = Environment(loader=FileSystemLoader(script_dir)) - template = env.get_template("episode.jinja") - logger.debug("Jinja environment set up and template loaded") - - # Render template with data - output = template.render(**data) - logger.debug("Template rendered with data") - - # Write to file - with open(output_file, "w") as f: - f.write(output) - logger.info(f"Output written to {get_relative_path(output_file)}") - - -if __name__ == "__main__": - main() diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/episode.jinja b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/episode.jinja deleted file mode 100644 index e35a6d9..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_page/episode.jinja +++ /dev/null @@ -1,29 +0,0 @@ -{# episode.jinja #} -{%- macro download_line(file, base_url, date) -%} -[Download {{ file }}]({{ base_url }}{{ date|replace('-', '/') }}/{{ file }}) -{%- endmacro -%} -public:: true -date-created:: [[{{ date_created }}]] -type:: [[Podcast/Episode]] - -- # {{ episode_title }} - {{ episode_date }} - - ![{{ audio_file }}](../assets/Ceremony/{{ episode_date|replace('-', '/') }}/{{ audio_file }}) -{%- if description %} -- ## Description - - {{ description }} -{%- endif %} -{%- if patch_files or midi_files %} -- ## Files -{%- if patch_files %} - - [[Microfreak/Patch]] files -{%- for patch in patch_files %} - - {{ download_line(patch, assets_base_url, episode_date) }} -{%- endfor %} -{%- endif %} -{%- if midi_files %} - - [[Microfreak/MIDI]] files -{%- for midi in midi_files %} - - {{ download_line(midi, assets_base_url, episode_date) }} -{%- endfor %} -{%- endif %} -{%- endif %} diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/.gitignore b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/.gitignore deleted file mode 100644 index 1f84087..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/.gitignore +++ /dev/null @@ -1 +0,0 @@ -new_reference_episode.md \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/constants.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/constants.py deleted file mode 100644 index 71dc1fd..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/constants.py +++ /dev/null @@ -1,16 +0,0 @@ -from datetime import datetime -from gitp_acolyte.constants import DATE_FORMAT, REFERENCE_DIR - -REFERENCE_EPISODE_DATE_STR = "2024-12-04" - -REFERENCE_EPISODE_DATE = datetime.strptime(REFERENCE_EPISODE_DATE_STR, DATE_FORMAT) - -DEFAULT_REFERENCE_EPISODE_YML_FILENAME = "reference_episode.yml" -DEFAULT_REFERENCE_EPISODE_YML_PATH = REFERENCE_DIR / DEFAULT_REFERENCE_EPISODE_YML_FILENAME - -# used -DRAFT_REFERENCE_EPISODE_OUTPUT_FILENAME = "new_reference_episode.md" -DRAFT_REFERENCE_EPISODE_OUTPUT_PATH = REFERENCE_DIR / DRAFT_REFERENCE_EPISODE_OUTPUT_FILENAME - -REFERENCE_PAGE_OUTPUT_FILENAME = "reference_episode.md" -REFERENCE_PAGE_OUTPUT_PATH = REFERENCE_DIR / REFERENCE_PAGE_OUTPUT_FILENAME \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/episode_schema.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/episode_schema.py deleted file mode 100644 index 5f16e0a..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/episode_schema.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List - -from pydantic import BaseModel, Field - - -class PodcastEpisodePublicationData(BaseModel): - """ - Data for a single published podcast episode. - """ - - episode_title: str = Field( - # fix BadRequestError 400 because `'default'` is not permitted in structured output - # default="Ceremony", - description="The title of the podcast episode; should default to 'Ceremony' if no further information is available." - ) - episode_date: str = Field( - ..., description="The publication date of the episode in YYYY-MM-DD format." - ) - audio_file: str = Field(..., description="The file name of the episode's audio.") - description: str = Field( - ..., - description="A text description of the episode content. Leave blank if not enough info to make a description is available.", - ) - patch_files: List[str] = Field( - default_factory=list, - description="Optional list of Microfreak synthesizer patch file names associated with this episode.", - ) - midi_files: List[str] = Field( - default_factory=list, - description="Optional list of MIDI file names associated with this episode.", - ) diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/.gitignore b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/.gitignore deleted file mode 100644 index b68c2c7..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/.gitignore +++ /dev/null @@ -1 +0,0 @@ -episode.yml \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.A.mfpz b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.A.mfpz deleted file mode 100644 index b63d760..0000000 Binary files a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.A.mfpz and /dev/null differ diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.B.mfpz b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.B.mfpz deleted file mode 100644 index 4f44309..0000000 Binary files a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.B.mfpz and /dev/null differ diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.C.mfpz b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.C.mfpz deleted file mode 100644 index b902c91..0000000 Binary files a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.C.mfpz and /dev/null differ diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.D.mfpz b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.D.mfpz deleted file mode 100644 index 81ea309..0000000 Binary files a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.D.mfpz and /dev/null differ diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.microfreak.mid b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.microfreak.mid deleted file mode 100644 index 28b8132..0000000 Binary files a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.microfreak.mid and /dev/null differ diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.mix.128kbps_CBR.mp3 b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.mix.128kbps_CBR.mp3 deleted file mode 100644 index d5c92a7..0000000 Binary files a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/GP.2024.12.04.mix.128kbps_CBR.mp3 and /dev/null differ diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/__init__.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/ref_ep_dir/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_episode.md b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_episode.md deleted file mode 100644 index f6bbdc0..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_episode.md +++ /dev/null @@ -1,16 +0,0 @@ -public:: true -date-created:: [[2024-12-04 Wed]] -type:: [[Podcast/Episode]] - -- # More Sequencer Fun - 2024-12-04 - - ![GP.2024.12.04.mix.128kbps_CBR.mp3](../assets/Ceremony/2024/12/04/GP.2024.12.04.mix.128kbps_CBR.mp3) -- ## Description - - Messing with the [[Microfreak/Sequencer]] -- ## Files - - [[Microfreak/Patch]] files - - [Download GP.2024.12.04.A.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.A.mfpz) - - [Download GP.2024.12.04.B.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.B.mfpz) - - [Download GP.2024.12.04.C.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.C.mfpz) - - [Download GP.2024.12.04.D.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.D.mfpz) - - [[Microfreak/MIDI]] files - - [Download GP.2024.12.04.microfreak.mid](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.microfreak.mid) \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_episode.yml b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_episode.yml deleted file mode 100644 index 38b8302..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_episode.yml +++ /dev/null @@ -1,11 +0,0 @@ -audio_file: GP.2024.12.04.mix.128kbps_CBR.mp3 -description: '' -episode_date: '2024-12-04' -episode_title: Ceremony -midi_files: -- GP.2024.12.04.microfreak.mid -patch_files: -- GP.2024.12.04.A.mfpz -- GP.2024.12.04.B.mfpz -- GP.2024.12.04.C.mfpz -- GP.2024.12.04.D.mfpz diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_output.md b/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_output.md deleted file mode 100644 index f6bbdc0..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/episode_reference/reference_output.md +++ /dev/null @@ -1,16 +0,0 @@ -public:: true -date-created:: [[2024-12-04 Wed]] -type:: [[Podcast/Episode]] - -- # More Sequencer Fun - 2024-12-04 - - ![GP.2024.12.04.mix.128kbps_CBR.mp3](../assets/Ceremony/2024/12/04/GP.2024.12.04.mix.128kbps_CBR.mp3) -- ## Description - - Messing with the [[Microfreak/Sequencer]] -- ## Files - - [[Microfreak/Patch]] files - - [Download GP.2024.12.04.A.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.A.mfpz) - - [Download GP.2024.12.04.B.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.B.mfpz) - - [Download GP.2024.12.04.C.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.C.mfpz) - - [Download GP.2024.12.04.D.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.D.mfpz) - - [[Microfreak/MIDI]] files - - [Download GP.2024.12.04.microfreak.mid](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GP.2024.12.04.microfreak.mid) \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/recording/files/pub_rec_files.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/recording/files/pub_rec_files.py deleted file mode 100644 index 6a0cb05..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/recording/files/pub_rec_files.py +++ /dev/null @@ -1,145 +0,0 @@ -import argparse -import logging -import re -import sys -from pathlib import Path - -import coloredlogs -from gitp_acolyte.ceremonial.spells.episode_data.args import ( - define_common_args, - get_episode_date, -) -from gitp_acolyte.ceremonial.spells.episode_data.create import ( - episode_publishing_dir_exists, - recording_dir_exists, -) -from gitp_acolyte.ceremonial.spells.recording.files.rec_file_constants import ( - FILE_SUFFIXES_TO_SYNC, - REC_DIR_SUBDIRS_TO_SEARCH, -) -from gitp_acolyte.constants import get_relative_path - -# Configure logging -logger = logging.getLogger(__name__) -coloredlogs.install( - level="DEBUG", - logger=logger, - fmt="%(asctime)s - %(module)s - %(levelname)s - %(message)s", -) - - -HELP_DOCSTRING = """ -Sync files from the episode recording directory to the episode publishing directory. -""" - - -def define_args(): - """ - Defines argparse arguments. - """ - parser = argparse.ArgumentParser(description=HELP_DOCSTRING) - parser = define_common_args() - parser.add_argument( - "--dry-run", - action="store_true", - help="Simulate the file sync without making any changes.", - ) - return parser.parse_args() - - -def filter_files_to_publish(src_dir: Path) -> list[Path]: - """ - Iterates through files in src_dir and any subdirectories in - REC_DIR_SUBDIRS_TO_SEARCH, and only returns files - whose file names end with any of the strings in FILE_SUFFIXES_TO_SYNC. - """ - - def is_valid_file(file: Path) -> bool: - return file.is_file() and any( - file.name.endswith(suffix) for suffix in FILE_SUFFIXES_TO_SYNC - ) - - files_to_sync = [item for item in src_dir.iterdir() if is_valid_file(item)] - - for subdir in REC_DIR_SUBDIRS_TO_SEARCH: - subdir_path = src_dir / subdir - if subdir_path.exists() and subdir_path.is_dir(): - files_to_sync.extend( - item for item in subdir_path.iterdir() if is_valid_file(item) - ) - - return files_to_sync - - -def sanitize_destination_filename(path: Path) -> Path: - """ - Replaces emojis and other complex UTF-8 characters in the filename with underscores. - """ - sanitized_name = re.sub(r"[^\w\-.]", "_", path.name) - return path.with_name(sanitized_name) - - -def sync_files(src_dir: Path, dest_dir: Path, dry_run: bool): - """ - Sync files from the source directory to the destination directory, - first filtering files with filter_files_to_publish. This function - will only write files to the destination directory if the file does not - yet exist in the destination directory or if the file in the destination - directory is older than the file in the source directory. - The directory structure is not preserved, so any files that are returned - from filter_files_to_publish will be copied to the destination directory. - """ - files_to_publish = filter_files_to_publish(src_dir) - for file in files_to_publish: - dest_file = sanitize_destination_filename(dest_dir / file.name) - if not dest_file.exists(): - if dry_run: - logger.info(f"Would copy '{file}' to '{get_relative_path(dest_file)}'") - else: - logger.debug(f"Copying '{file}' to '{get_relative_path(dest_file)}'") - file.replace(dest_file) - elif file.stat().st_mtime > dest_file.stat().st_mtime: - if dry_run: - logger.info( - f"Would overwrite '{get_relative_path(dest_file)}' with newer file '{file}'" - ) - else: - logger.warning( - f"Overwriting '{get_relative_path(dest_file)}' with newer file '{file}'" - ) - file.replace(dest_file) - else: - logger.debug( - f"Skipping sync; '{get_relative_path(dest_file)}' is newer than source file '{file}'" - ) - - -def main(): - args = define_args() - episode_date = get_episode_date(args) - logger.debug(f"Episode date: {episode_date}") - - if not recording_dir_exists(episode_date, args): - logger.error("Recording directory does not exist.") - return - - src_dir, src_dir_exists = recording_dir_exists(episode_date, args) - if not src_dir_exists: - sys.exit(1) - dest_dir, dest_dir_exists = episode_publishing_dir_exists(episode_date, args) - if not dest_dir_exists: - sys.exit(1) - - logger.debug(f"Source directory: '{src_dir}'") - logger.debug(f"Destination directory: '{get_relative_path(dest_dir)}'") - - sync_files(src_dir, dest_dir, args.dry_run) - - if args.dry_run: - logger.info("Dry run complete. No files were synced.") - else: - logger.info("Files synced successfully.") - - -if __name__ == "__main__": - main() diff --git a/gitp-acolyte/gitp_acolyte/ceremonial/spells/recording/files/rec_file_constants.py b/gitp-acolyte/gitp_acolyte/ceremonial/spells/recording/files/rec_file_constants.py deleted file mode 100644 index 6757ff1..0000000 --- a/gitp-acolyte/gitp_acolyte/ceremonial/spells/recording/files/rec_file_constants.py +++ /dev/null @@ -1,11 +0,0 @@ -REC_DIR_SUBDIRS_TO_SEARCH = ["Gdrive"] - -MICROFREAK_PATCH_FILE_EXTENSION = ".mfpz" -MIDI_FILE_EXTENSION = ".mid" -EPISODE_PUBLISHING_MP3_FILE_EXTENSION = ".mix.128kbps_CBR.mp3" - -FILE_SUFFIXES_TO_SYNC = [ - EPISODE_PUBLISHING_MP3_FILE_EXTENSION, - MICROFREAK_PATCH_FILE_EXTENSION, - MIDI_FILE_EXTENSION, -] diff --git a/gitp-acolyte/gitp_acolyte/constants.py b/gitp-acolyte/gitp_acolyte/constants.py deleted file mode 100644 index c48fa92..0000000 --- a/gitp-acolyte/gitp_acolyte/constants.py +++ /dev/null @@ -1,32 +0,0 @@ -from pathlib import Path - -RECORDINGS_ROOT_FOLDER = Path("~/Documents/ableton/GitP") - -REPO_ROOT = Path(__file__).resolve().parent.parent.parent - -# gitp-acolyte paths -ACOLYTE_DIR = REPO_ROOT / "gitp-acolyte" -ACOLYTE_PACKAGE_DIR = ACOLYTE_DIR / "gitp_acolyte" -CEREMONIAL_DIR = ACOLYTE_PACKAGE_DIR / "ceremonial" -SPELLS_DIR = CEREMONIAL_DIR / "spells" -REFERENCE_DIR = SPELLS_DIR / "episode_reference" -REFERENCE_EPISODE_DIR = REFERENCE_DIR / "ref_ep_dir" - -LOGSEQ_FOLDER = REPO_ROOT / "gitp-garden" -LOGSEQ_ASSETS_FOLDER = LOGSEQ_FOLDER / "assets" -LOGSEQ_PAGES_FOLDER = LOGSEQ_FOLDER / "pages" - -GITHUB_USER_CONTENT_ASSETS_BASE_URL = ( - "https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/" -) - -DATE_FORMAT = "%Y-%m-%d" -SHORT_DATE_FORMAT = "%y.%m.%d" -EPISODE_YAML_FILENAME = "episode.yml" - - -def get_relative_path(path: Path) -> Path: - """ - Get the path relative to the repository root. - """ - return path.relative_to(REPO_ROOT) diff --git a/gitp-acolyte/gitp_acolyte/utils/__init__.py b/gitp-acolyte/gitp_acolyte/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/utils/ai/__init__.py b/gitp-acolyte/gitp_acolyte/utils/ai/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/gitp_acolyte/utils/ai/test_openai_connectivity.py b/gitp-acolyte/gitp_acolyte/utils/ai/test_openai_connectivity.py deleted file mode 100644 index 0562a62..0000000 --- a/gitp-acolyte/gitp_acolyte/utils/ai/test_openai_connectivity.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -from dotenv import load_dotenv -from openai import OpenAI - -def test_openai_connectivity(): - load_dotenv() - client = OpenAI( - # this is the default and can be omitted - # api_key=os.environ.get("OPENAI_API_KEY") - ) - - chat_completion = client.chat.completions.create( - messages=[ - { - "role": "user", - "content": "Say this is a test", - } - ], - model="gpt-4o-mini", - ) - print(chat_completion) - -def main(): - test_openai_connectivity() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/gitp-acolyte/gitp_acolyte/utils/ai/test_openai_structured_outputs.py b/gitp-acolyte/gitp_acolyte/utils/ai/test_openai_structured_outputs.py deleted file mode 100644 index b6f6e00..0000000 --- a/gitp-acolyte/gitp_acolyte/utils/ai/test_openai_structured_outputs.py +++ /dev/null @@ -1,41 +0,0 @@ -from pydantic import BaseModel -from openai import OpenAI -from dotenv import load_dotenv - - - -def test_openai_structured_outputs(): - load_dotenv() - client = OpenAI() - - class Step(BaseModel): - explanation: str - output: str - - class MathReasoning(BaseModel): - steps: list[Step] - final_answer: str - - system_prompt = "You are a helpful math tutor. Guide the user through the solution step by step." - print(f"{system_prompt=}") - user_prompt = "how can I solve 8x + 7 = -23" - print(f"{user_prompt=}") - - print("Calling OpenAI ...") - completion = client.beta.chat.completions.parse( - model="gpt-4o-2024-08-06", - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt} - ], - response_format=MathReasoning, - ) - - math_reasoning = completion.choices[0].message.parsed - print(f"{math_reasoning=}") - -def main(): - result = test_openai_structured_outputs() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/gitp-acolyte/poetry.lock b/gitp-acolyte/poetry.lock deleted file mode 100644 index 8373e9c..0000000 --- a/gitp-acolyte/poetry.lock +++ /dev/null @@ -1,986 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.8.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "black" -version = "21.12b0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.6.2" -files = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, -] - -[package.dependencies] -click = ">=7.1.2" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" -platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" -typing-extensions = {version = ">=3.10.0.0,<3.10.0.1 || >3.10.0.1", markers = "python_version >= \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2024.12.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coloredlogs" -version = "15.0.1" -description = "Colored terminal output for Python's logging module" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, - {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, -] - -[package.dependencies] -humanfriendly = ">=9.1" - -[package.extras] -cron = ["capturer (>=2.4)"] - -[[package]] -name = "coverage" -version = "7.6.10" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, -] - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] - -[package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "humanfriendly" -version = "10.0" -description = "Human friendly output for text interfaces using Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, - {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, -] - -[package.dependencies] -pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.5" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jiter" -version = "0.8.2" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jiter-0.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ca8577f6a413abe29b079bc30f907894d7eb07a865c4df69475e868d73e71c7b"}, - {file = "jiter-0.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b25bd626bde7fb51534190c7e3cb97cee89ee76b76d7585580e22f34f5e3f393"}, - {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c826a221851a8dc028eb6d7d6429ba03184fa3c7e83ae01cd6d3bd1d4bd17d"}, - {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d35c864c2dff13dfd79fb070fc4fc6235d7b9b359efe340e1261deb21b9fcb66"}, - {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f557c55bc2b7676e74d39d19bcb8775ca295c7a028246175d6a8b431e70835e5"}, - {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:580ccf358539153db147e40751a0b41688a5ceb275e6f3e93d91c9467f42b2e3"}, - {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af102d3372e917cffce49b521e4c32c497515119dc7bd8a75665e90a718bbf08"}, - {file = "jiter-0.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cadcc978f82397d515bb2683fc0d50103acff2a180552654bb92d6045dec2c49"}, - {file = "jiter-0.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ba5bdf56969cad2019d4e8ffd3f879b5fdc792624129741d3d83fc832fef8c7d"}, - {file = "jiter-0.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b94a33a241bee9e34b8481cdcaa3d5c2116f575e0226e421bed3f7a6ea71cff"}, - {file = "jiter-0.8.2-cp310-cp310-win32.whl", hash = "sha256:6e5337bf454abddd91bd048ce0dca5134056fc99ca0205258766db35d0a2ea43"}, - {file = "jiter-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:4a9220497ca0cb1fe94e3f334f65b9b5102a0b8147646118f020d8ce1de70105"}, - {file = "jiter-0.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2dd61c5afc88a4fda7d8b2cf03ae5947c6ac7516d32b7a15bf4b49569a5c076b"}, - {file = "jiter-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c710d657c8d1d2adbbb5c0b0c6bfcec28fd35bd6b5f016395f9ac43e878a15"}, - {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9584de0cd306072635fe4b89742bf26feae858a0683b399ad0c2509011b9dc0"}, - {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a90a923338531b7970abb063cfc087eebae6ef8ec8139762007188f6bc69a9f"}, - {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21974d246ed0181558087cd9f76e84e8321091ebfb3a93d4c341479a736f099"}, - {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32475a42b2ea7b344069dc1e81445cfc00b9d0e3ca837f0523072432332e9f74"}, - {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9931fd36ee513c26b5bf08c940b0ac875de175341cbdd4fa3be109f0492586"}, - {file = "jiter-0.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0820f4a3a59ddced7fce696d86a096d5cc48d32a4183483a17671a61edfddc"}, - {file = "jiter-0.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ffc86ae5e3e6a93765d49d1ab47b6075a9c978a2b3b80f0f32628f39caa0c88"}, - {file = "jiter-0.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5127dc1abd809431172bc3fbe8168d6b90556a30bb10acd5ded41c3cfd6f43b6"}, - {file = "jiter-0.8.2-cp311-cp311-win32.whl", hash = "sha256:66227a2c7b575720c1871c8800d3a0122bb8ee94edb43a5685aa9aceb2782d44"}, - {file = "jiter-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:cde031d8413842a1e7501e9129b8e676e62a657f8ec8166e18a70d94d4682855"}, - {file = "jiter-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e6ec2be506e7d6f9527dae9ff4b7f54e68ea44a0ef6b098256ddf895218a2f8f"}, - {file = "jiter-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76e324da7b5da060287c54f2fabd3db5f76468006c811831f051942bf68c9d44"}, - {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:180a8aea058f7535d1c84183c0362c710f4750bef66630c05f40c93c2b152a0f"}, - {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025337859077b41548bdcbabe38698bcd93cfe10b06ff66617a48ff92c9aec60"}, - {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecff0dc14f409599bbcafa7e470c00b80f17abc14d1405d38ab02e4b42e55b57"}, - {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd9fee7d0775ebaba131f7ca2e2d83839a62ad65e8e02fe2bd8fc975cedeb9e"}, - {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14601dcac4889e0a1c75ccf6a0e4baf70dbc75041e51bcf8d0e9274519df6887"}, - {file = "jiter-0.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92249669925bc1c54fcd2ec73f70f2c1d6a817928480ee1c65af5f6b81cdf12d"}, - {file = "jiter-0.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e725edd0929fa79f8349ab4ec7f81c714df51dc4e991539a578e5018fa4a7152"}, - {file = "jiter-0.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bf55846c7b7a680eebaf9c3c48d630e1bf51bdf76c68a5f654b8524335b0ad29"}, - {file = "jiter-0.8.2-cp312-cp312-win32.whl", hash = "sha256:7efe4853ecd3d6110301665a5178b9856be7e2a9485f49d91aa4d737ad2ae49e"}, - {file = "jiter-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:83c0efd80b29695058d0fd2fa8a556490dbce9804eac3e281f373bbc99045f6c"}, - {file = "jiter-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ca1f08b8e43dc3bd0594c992fb1fd2f7ce87f7bf0d44358198d6da8034afdf84"}, - {file = "jiter-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5672a86d55416ccd214c778efccf3266b84f87b89063b582167d803246354be4"}, - {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58dc9bc9767a1101f4e5e22db1b652161a225874d66f0e5cb8e2c7d1c438b587"}, - {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b2998606d6dadbb5ccda959a33d6a5e853252d921fec1792fc902351bb4e2c"}, - {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab9a87f3784eb0e098f84a32670cfe4a79cb6512fd8f42ae3d0709f06405d18"}, - {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79aec8172b9e3c6d05fd4b219d5de1ac616bd8da934107325a6c0d0e866a21b6"}, - {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:711e408732d4e9a0208008e5892c2966b485c783cd2d9a681f3eb147cf36c7ef"}, - {file = "jiter-0.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:653cf462db4e8c41995e33d865965e79641ef45369d8a11f54cd30888b7e6ff1"}, - {file = "jiter-0.8.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9c63eaef32b7bebac8ebebf4dabebdbc6769a09c127294db6babee38e9f405b9"}, - {file = "jiter-0.8.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:eb21aaa9a200d0a80dacc7a81038d2e476ffe473ffdd9c91eb745d623561de05"}, - {file = "jiter-0.8.2-cp313-cp313-win32.whl", hash = "sha256:789361ed945d8d42850f919342a8665d2dc79e7e44ca1c97cc786966a21f627a"}, - {file = "jiter-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ab7f43235d71e03b941c1630f4b6e3055d46b6cb8728a17663eaac9d8e83a865"}, - {file = "jiter-0.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b426f72cd77da3fec300ed3bc990895e2dd6b49e3bfe6c438592a3ba660e41ca"}, - {file = "jiter-0.8.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2dd880785088ff2ad21ffee205e58a8c1ddabc63612444ae41e5e4b321b39c0"}, - {file = "jiter-0.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566"}, - {file = "jiter-0.8.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9e1fa156ee9454642adb7e7234a383884452532bc9d53d5af2d18d98ada1d79c"}, - {file = "jiter-0.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cf5dfa9956d96ff2efb0f8e9c7d055904012c952539a774305aaaf3abdf3d6c"}, - {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e52bf98c7e727dd44f7c4acb980cb988448faeafed8433c867888268899b298b"}, - {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a2ecaa3c23e7a7cf86d00eda3390c232f4d533cd9ddea4b04f5d0644faf642c5"}, - {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08d4c92bf480e19fc3f2717c9ce2aa31dceaa9163839a311424b6862252c943e"}, - {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99d9a1eded738299ba8e106c6779ce5c3893cffa0e32e4485d680588adae6db8"}, - {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d20be8b7f606df096e08b0b1b4a3c6f0515e8dac296881fe7461dfa0fb5ec817"}, - {file = "jiter-0.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d33f94615fcaf872f7fd8cd98ac3b429e435c77619777e8a449d9d27e01134d1"}, - {file = "jiter-0.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:317b25e98a35ffec5c67efe56a4e9970852632c810d35b34ecdd70cc0e47b3b6"}, - {file = "jiter-0.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc9043259ee430ecd71d178fccabd8c332a3bf1e81e50cae43cc2b28d19e4cb7"}, - {file = "jiter-0.8.2-cp38-cp38-win32.whl", hash = "sha256:fc5adda618205bd4678b146612ce44c3cbfdee9697951f2c0ffdef1f26d72b63"}, - {file = "jiter-0.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cd646c827b4f85ef4a78e4e58f4f5854fae0caf3db91b59f0d73731448a970c6"}, - {file = "jiter-0.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e41e75344acef3fc59ba4765df29f107f309ca9e8eace5baacabd9217e52a5ee"}, - {file = "jiter-0.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f22b16b35d5c1df9dfd58843ab2cd25e6bf15191f5a236bed177afade507bfc"}, - {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7200b8f7619d36aa51c803fd52020a2dfbea36ffec1b5e22cab11fd34d95a6d"}, - {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70bf4c43652cc294040dbb62256c83c8718370c8b93dd93d934b9a7bf6c4f53c"}, - {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9d471356dc16f84ed48768b8ee79f29514295c7295cb41e1133ec0b2b8d637d"}, - {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:859e8eb3507894093d01929e12e267f83b1d5f6221099d3ec976f0c995cb6bd9"}, - {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa58399c01db555346647a907b4ef6d4f584b123943be6ed5588c3f2359c9f4"}, - {file = "jiter-0.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f2d5ed877f089862f4c7aacf3a542627c1496f972a34d0474ce85ee7d939c27"}, - {file = "jiter-0.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:03c9df035d4f8d647f8c210ddc2ae0728387275340668fb30d2421e17d9a0841"}, - {file = "jiter-0.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8bd2a824d08d8977bb2794ea2682f898ad3d8837932e3a74937e93d62ecbb637"}, - {file = "jiter-0.8.2-cp39-cp39-win32.whl", hash = "sha256:ca29b6371ebc40e496995c94b988a101b9fbbed48a51190a4461fcb0a68b4a36"}, - {file = "jiter-0.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1c0dfbd1be3cbefc7510102370d86e35d1d53e5a93d48519688b1bf0f761160a"}, - {file = "jiter-0.8.2.tar.gz", hash = "sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = "*" -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "openai" -version = "1.59.3" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "openai-1.59.3-py3-none-any.whl", hash = "sha256:b041887a0d8f3e70d1fc6ffbb2bf7661c3b9a2f3e806c04bf42f572b9ac7bc37"}, - {file = "openai-1.59.3.tar.gz", hash = "sha256:7f7fff9d8729968588edf1524e73266e8593bb6cab09298340efb755755bb66f"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<15)"] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "psutil" -version = "6.1.1" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, - {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, - {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"}, - {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"}, - {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"}, - {file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"}, - {file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"}, - {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"}, - {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"}, - {file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"}, - {file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"}, - {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"}, - {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"}, - {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] - -[[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] - -[[package]] -name = "pydantic" -version = "2.10.4" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pyflakes" -version = "2.3.1" -description = "passive checker of Python programs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] - -[[package]] -name = "pyreadline3" -version = "3.5.4" -description = "A python implementation of GNU readline." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, - {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, -] - -[package.extras] -dev = ["build", "flake8", "mypy", "pytest", "twine"] - -[[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "6.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, -] - -[package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[package.dependencies] -execnet = ">=2.1" -psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "tomli" -version = "1.2.3" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.12" -content-hash = "15468356b551e01d831f67a10db0934bbc482bbe41163ac7d8a951078ac29534" diff --git a/gitp-acolyte/pyproject.toml b/gitp-acolyte/pyproject.toml deleted file mode 100644 index dac8c0b..0000000 --- a/gitp-acolyte/pyproject.toml +++ /dev/null @@ -1,31 +0,0 @@ -[tool.poetry] -name = "gitp-acolyte" -version = "0.1.0" -description = "A set of scripts for compiling the Ghost in the Patch podcast and website" -authors = ["Codekiln "] -license = "MIT" -readme = "README.md" -homepage = "https://github.com/codekiln/gitpa" -repository = "https://github.com/codekiln/gitpa" -documentation = "https://github.com/codekiln/gitpa" - -[tool.poetry.dependencies] -python = "^3.12" -pyyaml = "^6.0.2" -coloredlogs = "^15.0.1" -jinja2 = "^3.1.4" -python-dotenv = "^1.0.1" -openai = "^1.58.1" - -[tool.poetry.group.dev.dependencies] -pytest-xdist = { version = "^3.6.1", extras = ["psutil"] } -black = "^21.9b0" -flake8 = "^3.9.2" -pytest-cov = "^6.0.0" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - - [tool.pytest.ini_options] - addopts="-n 4" \ No newline at end of file diff --git a/gitp-acolyte/tests/__init__.py b/gitp-acolyte/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/tests/ceremonial/__init__.py b/gitp-acolyte/tests/ceremonial/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/tests/ceremonial/spells/__init__.py b/gitp-acolyte/tests/ceremonial/spells/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/tests/ceremonial/spells/recording/__init__.py b/gitp-acolyte/tests/ceremonial/spells/recording/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/tests/ceremonial/spells/recording/files/__init__.py b/gitp-acolyte/tests/ceremonial/spells/recording/files/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitp-acolyte/tests/ceremonial/spells/recording/files/test_pub_rec_files.py b/gitp-acolyte/tests/ceremonial/spells/recording/files/test_pub_rec_files.py deleted file mode 100644 index c335d80..0000000 --- a/gitp-acolyte/tests/ceremonial/spells/recording/files/test_pub_rec_files.py +++ /dev/null @@ -1,88 +0,0 @@ -from pathlib import Path - -import pytest -from gitp_acolyte.ceremonial.spells.recording.files.pub_rec_files import ( - filter_files_to_publish, -) -from gitp_acolyte.ceremonial.spells.recording.files.rec_file_constants import ( - FILE_SUFFIXES_TO_SYNC, - REC_DIR_SUBDIRS_TO_SEARCH, -) - - -def create_mock_files(base_path: Path, subdirs: list[str] = None): - if subdirs is None: - subdirs = [""] - for subdir in subdirs: - subdir_path = base_path / subdir - subdir_path.mkdir(parents=True, exist_ok=True) - for i, suffix in enumerate(FILE_SUFFIXES_TO_SYNC): - (subdir_path / f"file{i}{suffix}").touch() - (subdir_path / "file.invalid").touch() - - -@pytest.fixture -def mock_src_dir_root(tmp_path): - create_mock_files(tmp_path) - return tmp_path - - -@pytest.fixture -def mock_src_dir_subdirs(tmp_path): - create_mock_files(tmp_path, REC_DIR_SUBDIRS_TO_SEARCH) - return tmp_path - - -def test_filter_files_to_publish_all_valid_root(mock_src_dir_root): - valid_files = filter_files_to_publish(mock_src_dir_root) - expected_files = [ - mock_src_dir_root / f"file{i}{suffix}" - for i, suffix in enumerate(FILE_SUFFIXES_TO_SYNC) - ] - assert sorted(valid_files, key=str) == sorted(expected_files, key=str) - - -def test_filter_files_to_publish_all_valid_subdirs(mock_src_dir_subdirs): - valid_files = filter_files_to_publish(mock_src_dir_subdirs) - expected_files = [ - mock_src_dir_subdirs / subdir / f"file{i}{suffix}" - for subdir in REC_DIR_SUBDIRS_TO_SEARCH - for i, suffix in enumerate(FILE_SUFFIXES_TO_SYNC) - ] - assert sorted(valid_files, key=str) == sorted(expected_files, key=str) - - -def test_filter_files_to_publish_some_invalid_root(mock_src_dir_root): - valid_files = filter_files_to_publish(mock_src_dir_root) - expected_files = [ - mock_src_dir_root / f"file{i}{suffix}" - for i, suffix in enumerate(FILE_SUFFIXES_TO_SYNC) - ] - assert sorted(valid_files, key=str) == sorted(expected_files, key=str) - - -def test_filter_files_to_publish_some_invalid_subdirs(mock_src_dir_subdirs): - valid_files = filter_files_to_publish(mock_src_dir_subdirs) - expected_files = [ - mock_src_dir_subdirs / subdir / f"file{i}{suffix}" - for subdir in REC_DIR_SUBDIRS_TO_SEARCH - for i, suffix in enumerate(FILE_SUFFIXES_TO_SYNC) - ] - assert sorted(valid_files, key=str) == sorted(expected_files, key=str) - - -def test_filter_files_to_publish_all_invalid(tmp_path): - (tmp_path / "file.invalid").touch() - for subdir in REC_DIR_SUBDIRS_TO_SEARCH: - subdir_path = tmp_path / subdir - subdir_path.mkdir(parents=True, exist_ok=True) - (subdir_path / "file.invalid").touch() - valid_files = filter_files_to_publish(tmp_path) - assert valid_files == [] - - -def test_filter_files_to_publish_empty_list(tmp_path): - for subdir in REC_DIR_SUBDIRS_TO_SEARCH: - (tmp_path / subdir).mkdir(parents=True, exist_ok=True) - valid_files = filter_files_to_publish(tmp_path) - assert valid_files == [] diff --git a/gitp-garden/.logseq-proxy/manifest.json b/gitp-garden/.logseq-proxy/manifest.json new file mode 100644 index 0000000..7359fd1 --- /dev/null +++ b/gitp-garden/.logseq-proxy/manifest.json @@ -0,0 +1,362 @@ +{ + "declarations": { + "GitP/A/Session/24/11/19-Tue": { + "logseq-entity": "[[Logseq/Entity/Podcast/Episode]]" + }, + "GitP/A/Session/24/11/19-Tue/Asset/Artwork/Overview/gif": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/11/19-Tue/Asset/Preset/A/mfpz": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/11/19-Tue/Asset/Preset/B/mfpz": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/11/19-Tue/Asset/Preset/C/mfpz": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/11/19-Tue/Asset/Synth/Full/mp3": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/12/04-Wed": { + "logseq-entity": "[[Logseq/Entity/Podcast/Episode]]" + }, + "GitP/A/Session/24/12/04-Wed/Asset/Artwork/Overview/gif": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/12/04-Wed/Asset/MIDI/Microfreak/mid": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/12/04-Wed/Asset/Preset/A/mfpz": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/12/04-Wed/Asset/Preset/B/mfpz": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/12/04-Wed/Asset/Preset/C/mfpz": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/12/04-Wed/Asset/Preset/D/mfpz": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/24/12/04-Wed/Asset/Synth/Full/mp3": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/26/09/24-Thu": { + "logseq-entity": "[[Logseq/Entity/Podcast/Episode]]" + }, + "GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "GitP/A/Session/26/09/25-Fri": { + "logseq-entity": "[[Logseq/Entity/Podcast/Episode]]" + }, + "GitP/A/Session/26/09/25-Fri/Asset/Synth/Full/mp3": { + "logseq-entity": "[[Logseq/Entity/Asset/B2]]" + }, + "Logseq/Entity/Asset/B2": { + "logseq-entity": "[[Logseq/Entity/Definition]]" + }, + "Logseq/Entity/Definition": { + "logseq-entity": "[[Logseq/Entity/Definition]]" + }, + "Logseq/Entity/Mise/Task": { + "logseq-entity": "[[Logseq/Entity/Definition]]" + }, + "Logseq/Entity/Podcast/Episode": { + "logseq-entity": "[[Logseq/Entity/Definition]]" + }, + "Logseq/Entity/Proxy/Page": { + "entity-tasks": "[[Logseq/Entity/Proxy/Page/mise/Task/sync]]", + "logseq-entity": "[[Logseq/Entity/Definition]]" + }, + "Logseq/Entity/Proxy/Page/mise/Task/sync": { + "logseq-entity": "[[Logseq/Entity/Mise/Task]]", + "source-link": "https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/sync", + "task-config-root": ".", + "task-entrypoint": "mise-tasks/logseq/entity/proxy/page/sync", + "task-files": "{\"mise-tasks/logseq/entity/proxy/page/sync\":\"mise-tasks/logseq/entity/proxy/page/sync\",\"mise-tasks/logseq/entity/proxy/page/lib/core.py\":\"mise-tasks/logseq/entity/proxy/page/lib/core.py\",\"mise-tasks/logseq/entity/proxy/page/lib/companions.py\":\"mise-tasks/logseq/entity/proxy/page/lib/companions.py\"}", + "task-name": "logseq:entity:proxy:page:sync", + "task-owner": "[[Person/codekiln/GitHub/logseq-encode-garden]]" + } + }, + "files": { + "mise-tasks/logseq/entity/proxy/page/lib/companions.py": { + "mode": 420, + "sha256": "9527f6bca636edf9c9425c5c774c4333ed028025cdc56de849e0828dc703fc87", + "source": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "mise-tasks/logseq/entity/proxy/page/lib/companions.py" + ] + }, + "mise-tasks/logseq/entity/proxy/page/lib/core.py": { + "mode": 420, + "sha256": "162f58b6461b5d6b37ea9a23a75e17013b5d03ec7049b065b56c8347ff2d95ee", + "source": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "mise-tasks/logseq/entity/proxy/page/lib/core.py" + ] + }, + "mise-tasks/logseq/entity/proxy/page/sync": { + "mode": 493, + "sha256": "4dc4adbcd690cb3d37fac58a7471454dc019d57160e8afc609d2508b1632a5ff", + "source": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "mise-tasks/logseq/entity/proxy/page/sync" + ] + } + }, + "imports": { + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/11/19-Tue": { + "files": [], + "page": "GitP/A/Session/24/11/19-Tue", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/11/19-Tue/Asset/Artwork/Overview/gif": { + "files": [], + "page": "GitP/A/Session/24/11/19-Tue/Asset/Artwork/Overview/gif", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/11/19-Tue/Asset/Preset/A/mfpz": { + "files": [], + "page": "GitP/A/Session/24/11/19-Tue/Asset/Preset/A/mfpz", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/11/19-Tue/Asset/Preset/B/mfpz": { + "files": [], + "page": "GitP/A/Session/24/11/19-Tue/Asset/Preset/B/mfpz", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/11/19-Tue/Asset/Preset/C/mfpz": { + "files": [], + "page": "GitP/A/Session/24/11/19-Tue/Asset/Preset/C/mfpz", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/11/19-Tue/Asset/Synth/Full/mp3": { + "files": [], + "page": "GitP/A/Session/24/11/19-Tue/Asset/Synth/Full/mp3", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed/Asset/Artwork/Overview/gif": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed/Asset/Artwork/Overview/gif", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed/Asset/MIDI/Microfreak/mid": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed/Asset/MIDI/Microfreak/mid", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed/Asset/Preset/A/mfpz": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed/Asset/Preset/A/mfpz", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed/Asset/Preset/B/mfpz": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed/Asset/Preset/B/mfpz", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed/Asset/Preset/C/mfpz": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed/Asset/Preset/C/mfpz", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed/Asset/Preset/D/mfpz": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed/Asset/Preset/D/mfpz", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/24/12/04-Wed/Asset/Synth/Full/mp3": { + "files": [], + "page": "GitP/A/Session/24/12/04-Wed/Asset/Synth/Full/mp3", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/26/09/24-Thu": { + "files": [], + "page": "GitP/A/Session/26/09/24-Thu", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3": { + "files": [], + "page": "GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/26/09/25-Fri": { + "files": [], + "page": "GitP/A/Session/26/09/25-Fri", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::GitP/A/Session/26/09/25-Fri/Asset/Synth/Full/mp3": { + "files": [], + "page": "GitP/A/Session/26/09/25-Fri/Asset/Synth/Full/mp3", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::Logseq/Entity/Asset/B2": { + "files": [], + "page": "Logseq/Entity/Asset/B2", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::Logseq/Entity/Podcast/Episode": { + "files": [], + "page": "Logseq/Entity/Podcast/Episode", + "source_graph": "." + }, + "github.com/codekiln/logseq-encode-garden::.::Logseq/Entity/Proxy/Page": { + "files": [ + "mise-tasks/logseq/entity/proxy/page/lib/companions.py", + "mise-tasks/logseq/entity/proxy/page/lib/core.py", + "mise-tasks/logseq/entity/proxy/page/sync" + ], + "page": "Logseq/Entity/Proxy/Page", + "source_graph": "." + } + }, + "pages": { + "GitP/A/Session/24/11/19-Tue": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/11/19-Tue" + ], + "GitP/A/Session/24/11/19-Tue/Asset/Artwork/Overview/gif": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/11/19-Tue/Asset/Artwork/Overview/gif" + ], + "GitP/A/Session/24/11/19-Tue/Asset/Preset/A/mfpz": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/11/19-Tue/Asset/Preset/A/mfpz" + ], + "GitP/A/Session/24/11/19-Tue/Asset/Preset/B/mfpz": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/11/19-Tue/Asset/Preset/B/mfpz" + ], + "GitP/A/Session/24/11/19-Tue/Asset/Preset/C/mfpz": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/11/19-Tue/Asset/Preset/C/mfpz" + ], + "GitP/A/Session/24/11/19-Tue/Asset/Synth/Full/mp3": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/11/19-Tue/Asset/Synth/Full/mp3" + ], + "GitP/A/Session/24/12/04-Wed": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed" + ], + "GitP/A/Session/24/12/04-Wed/Asset/Artwork/Overview/gif": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed/Asset/Artwork/Overview/gif" + ], + "GitP/A/Session/24/12/04-Wed/Asset/MIDI/Microfreak/mid": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed/Asset/MIDI/Microfreak/mid" + ], + "GitP/A/Session/24/12/04-Wed/Asset/Preset/A/mfpz": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed/Asset/Preset/A/mfpz" + ], + "GitP/A/Session/24/12/04-Wed/Asset/Preset/B/mfpz": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed/Asset/Preset/B/mfpz" + ], + "GitP/A/Session/24/12/04-Wed/Asset/Preset/C/mfpz": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed/Asset/Preset/C/mfpz" + ], + "GitP/A/Session/24/12/04-Wed/Asset/Preset/D/mfpz": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed/Asset/Preset/D/mfpz" + ], + "GitP/A/Session/24/12/04-Wed/Asset/Synth/Full/mp3": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/24/12/04-Wed/Asset/Synth/Full/mp3" + ], + "GitP/A/Session/26/09/24-Thu": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/26/09/24-Thu" + ], + "GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3" + ], + "GitP/A/Session/26/09/25-Fri": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/26/09/25-Fri" + ], + "GitP/A/Session/26/09/25-Fri/Asset/Synth/Full/mp3": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "GitP/A/Session/26/09/25-Fri/Asset/Synth/Full/mp3" + ], + "Logseq/Entity/Asset/B2": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "Logseq/Entity/Asset/B2" + ], + "Logseq/Entity/Definition": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "Logseq/Entity/Definition" + ], + "Logseq/Entity/Mise/Task": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "Logseq/Entity/Mise/Task" + ], + "Logseq/Entity/Podcast/Episode": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "Logseq/Entity/Podcast/Episode" + ], + "Logseq/Entity/Proxy/Page": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "Logseq/Entity/Proxy/Page" + ], + "Logseq/Entity/Proxy/Page/mise/Task/sync": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "Logseq/Entity/Proxy/Page/mise/Task/sync" + ] + }, + "tasks": { + "Logseq/Entity/Proxy/Page/mise/Task/sync": { + "identity": [ + "github.com/codekiln/logseq-encode-garden", + ".", + "[[Person/codekiln/GitHub/logseq-encode-garden]]", + ".", + "logseq:entity:proxy:page:sync" + ], + "mapping": { + "mise-tasks/logseq/entity/proxy/page/lib/companions.py": "mise-tasks/logseq/entity/proxy/page/lib/companions.py", + "mise-tasks/logseq/entity/proxy/page/lib/core.py": "mise-tasks/logseq/entity/proxy/page/lib/core.py", + "mise-tasks/logseq/entity/proxy/page/sync": "mise-tasks/logseq/entity/proxy/page/sync" + } + } + }, + "version": 1 +} diff --git a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.A.mfpatch.mfpz b/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.A.mfpatch.mfpz deleted file mode 100644 index 3af3f05..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.A.mfpatch.mfpz and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.B.mfpatch.mfpz b/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.B.mfpatch.mfpz deleted file mode 100644 index 12b75d8..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.B.mfpatch.mfpz and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.C.mfpatch.mfpz b/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.C.mfpatch.mfpz deleted file mode 100644 index e160fee..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.18.C.mfpatch.mfpz and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.19.mix.128kbps_CBR.mp3 b/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.19.mix.128kbps_CBR.mp3 deleted file mode 100644 index f4cc060..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/11/19/GitP.2024.11.19.mix.128kbps_CBR.mp3 and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/11/19/episode.yml b/gitp-garden/assets/Ceremony/2024/11/19/episode.yml deleted file mode 100644 index a1c4778..0000000 --- a/gitp-garden/assets/Ceremony/2024/11/19/episode.yml +++ /dev/null @@ -1,9 +0,0 @@ -audio_file: GitP.2024.11.19.mix.128kbps_CBR.mp3 -description: '' -episode_date: '2024-11-19' -episode_title: Ceremony -midi_files: [] -patch_files: -- GitP.2024.11.18.A.mfpatch.mfpz -- GitP.2024.11.18.B.mfpatch.mfpz -- GitP.2024.11.18.C.mfpatch.mfpz diff --git a/gitp-garden/assets/Ceremony/2024/11/19/furry-onesie-rainbow-goblin-2.gif b/gitp-garden/assets/Ceremony/2024/11/19/furry-onesie-rainbow-goblin-2.gif deleted file mode 100644 index dd96407..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/11/19/furry-onesie-rainbow-goblin-2.gif and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.A.mfpz b/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.A.mfpz deleted file mode 100644 index b63d760..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.A.mfpz and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.B.mfpz b/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.B.mfpz deleted file mode 100644 index 4f44309..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.B.mfpz and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.C.mfpz b/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.C.mfpz deleted file mode 100644 index b902c91..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.C.mfpz and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.D.mfpz b/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.D.mfpz deleted file mode 100644 index 81ea309..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.D.mfpz and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.microfreak.mid b/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.microfreak.mid deleted file mode 100644 index 28b8132..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.microfreak.mid and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.mix.128kbps_CBR.mp3 b/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.mix.128kbps_CBR.mp3 deleted file mode 100644 index d5c92a7..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/12/04/GitP.2024.12.04.mix.128kbps_CBR.mp3 and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2024/12/04/episode.yml b/gitp-garden/assets/Ceremony/2024/12/04/episode.yml deleted file mode 100644 index 97ff851..0000000 --- a/gitp-garden/assets/Ceremony/2024/12/04/episode.yml +++ /dev/null @@ -1,11 +0,0 @@ -audio_file: GitP.2024.12.04.mix.128kbps_CBR.mp3 -description: '' -episode_date: '2024-12-04' -episode_title: Ceremony -midi_files: -- GitP.2024.12.04.microfreak.mid -patch_files: -- GitP.2024.12.04.A.mfpz -- GitP.2024.12.04.B.mfpz -- GitP.2024.12.04.C.mfpz -- GitP.2024.12.04.D.mfpz diff --git a/gitp-garden/assets/Ceremony/2024/12/04/furry-onesie-rainbow-goblin.gif b/gitp-garden/assets/Ceremony/2024/12/04/furry-onesie-rainbow-goblin.gif deleted file mode 100644 index 174b76f..0000000 Binary files a/gitp-garden/assets/Ceremony/2024/12/04/furry-onesie-rainbow-goblin.gif and /dev/null differ diff --git a/gitp-garden/assets/Ceremony/2026/09/24/episode.yml b/gitp-garden/assets/Ceremony/2026/09/24/episode.yml deleted file mode 100644 index 3ecb1b5..0000000 --- a/gitp-garden/assets/Ceremony/2026/09/24/episode.yml +++ /dev/null @@ -1,8 +0,0 @@ -published: false -episode_title: GitP.26.09.24 -description: "A MicroFreak and Launchpad session beginning with two-operator FM on an initialized patch." -recorded_on: '2026-09-24' -audio_url: https://f005.backblazeb2.com/file/logseq-encode-garden/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full.mp3 -audio_length: 18188163 -audio_type: audio/mpeg -page: Ceremony/2026/09/24 diff --git a/gitp-garden/assets/Ceremony/2026/09/25/episode.yml b/gitp-garden/assets/Ceremony/2026/09/25/episode.yml deleted file mode 100644 index 4d4a803..0000000 --- a/gitp-garden/assets/Ceremony/2026/09/25/episode.yml +++ /dev/null @@ -1,11 +0,0 @@ -published: true -episode_title: GitP.26.09.25 -description: MicroFreak SAWX and WaveUser preset 1910, with a Novation Launchpad sequence. -recorded_on: '2026-09-25' -published_at: '2026-10-03T12:00:00-04:00' -guid: gitpa20260925 -page: Ceremony/2026/09/25 -page_url: https://codekiln.github.io/gitpa/#/page/Ceremony%2F2026%2F09%2F25 -audio_url: https://f005.backblazeb2.com/file/logseq-encode-garden/gitpa/episodes/2026-09-25/GitP.26.09.25.mp3 -audio_length: 23569806 -audio_type: audio/mpeg diff --git a/gitp-garden/journals/2025_01_03.md b/gitp-garden/journals/2025_01_03.md deleted file mode 100644 index 58fd9b9..0000000 --- a/gitp-garden/journals/2025_01_03.md +++ /dev/null @@ -1,4 +0,0 @@ -- Trying some sora prompts for a ghost in the patch video - - ![gitp_logo_raw_fly.JPG](../assets/gitp/logo/gitp_logo_raw_fly.JPG){:height 778, :width 770} - - - - Nyxis the Harmonic Starling came to the Guild of the Ghostpatch in the The Glen of Resonant Whispers from another planet. They are fused with a starsynth hovercraft that they drive around the glen during ceremonies. Nyxis's antennae pick up the sounds of the stars, and as they circle around the glen, light thickens in a trail behind them, descending on the trees below like long strands of glowing hot toffee. \ No newline at end of file diff --git a/gitp-garden/logseq/config.edn b/gitp-garden/logseq/config.edn index 866d41f..8d15cd2 100644 --- a/gitp-garden/logseq/config.edn +++ b/gitp-garden/logseq/config.edn @@ -15,7 +15,7 @@ ;; Exclude directories/files. ;; Example usage: ;; :hidden ["/archived" "/test.md" "../assets/archived"] - :hidden ["script" "README.md" "CONTRIBUTING.md" "LICENSE.md", "gitp-acolyte"] + :hidden ["script" "README.md" "CONTRIBUTING.md" "LICENSE.md"] ;; Define the default journal page template. ;; Enter the template name between the quotes. @@ -315,7 +315,12 @@ ;; Hide specific block properties. ;; Example usage: - ;;:block-hidden-properties #{:public :icon} + :block-hidden-properties #{:public :tags :alias :created-by :duration-seconds + :podcast-guid :podcast-published-at :see-also :source-link + :logseq-proxy-url :logseq-proxy-codeforge-url + :logseq-proxy-last-sync-date :entity-proxy-destination-properties + :entity-tasks :task-owner :task-config-root :task-name + :task-entrypoint :task-files} ;; Create a page for all properties. ;; Default value: true diff --git a/gitp-garden/logseq/custom.css b/gitp-garden/logseq/custom.css index ec091f7..dd685b0 100644 --- a/gitp-garden/logseq/custom.css +++ b/gitp-garden/logseq/custom.css @@ -1,2 +1,6 @@ /* This uses the logseq-dev-theme but the url can be changed to any theme's github url */ -@import url("https://cdn.jsdelivr.net/gh/henices/logseq-flow-nord@main/nord.css"); \ No newline at end of file +@import url("https://cdn.jsdelivr.net/gh/henices/logseq-flow-nord@main/nord.css"); +/* Publication properties are used by queries, not by the episode layout. + Hiding the whole property block also removes its empty bullet. The desktop + graph displays the properties selected in config.edn. */ +.is-publish-mode .ls-block.pre-block:has(.page-properties) { display: none; } diff --git a/gitp-garden/mise-tasks/logseq/entity/proxy/page/lib/companions.py b/gitp-garden/mise-tasks/logseq/entity/proxy/page/lib/companions.py new file mode 100644 index 0000000..7e67f6e --- /dev/null +++ b/gitp-garden/mise-tasks/logseq/entity/proxy/page/lib/companions.py @@ -0,0 +1,269 @@ +"""Import explicitly declared file tasks with conservative update ownership.""" +from __future__ import annotations + +import hashlib +import json +from pathlib import Path, PurePosixPath +import re +import stat +import subprocess +from urllib.parse import urlsplit +import tomllib + +import core + +MANIFEST = '.logseq-proxy/manifest.json' + + +def refs(value): + return re.findall(r'\[\[([^\]]+)\]\]', value or '') + + +def repository(graph): + for parent in (graph, *graph.parents): + if (parent / '.git').exists(): + return parent + return graph + + +def repository_identity(repo): + result = subprocess.run(['git', '-C', str(repo), 'remote', 'get-url', 'origin'], capture_output=True, text=True) + if result.returncode == 0: + remote = result.stdout.strip() + if '://' in remote: + parsed = urlsplit(remote) + address = (parsed.hostname or '') + '/' + parsed.path.lstrip('/') + elif ':' in remote and not remote.startswith('/'): + host, path = remote.split(':', 1) + address = host.split('@')[-1] + '/' + path + else: + address = remote + return address.removesuffix('.git').rstrip('/') + return str(repo.resolve()) + + +def relative(value, label): + path = PurePosixPath(value) + if not value or path == PurePosixPath('.') or path.is_absolute() or '..' in path.parts or '\\' in value or path.as_posix() != value: + raise ValueError(f'{label}: expected a normalized repository-relative path: {value}') + return path + + +def safe_file(root, value, label): + path = relative(value, label) + current = root + for part in path.parts: + current = current / part + if current.is_symlink(): + raise ValueError(f'{label}: symlink is unsupported: {value}') + return current + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def compatible_config(root): + for filename in ('mise.toml', '.mise.toml'): + config = root / filename + if config.exists(): + parsed = tomllib.loads(config.read_text()) + includes = parsed.get('task_config', {}).get('includes') + if includes is not None and not any(str(value).rstrip('/') in ('mise-tasks', './mise-tasks') for value in includes): + raise ValueError(f'{config}: task_config.includes must include mise-tasks to discover imported tasks') + return + + +def load_manifest(data, label): + try: + manifest = json.loads(data) + if not isinstance(manifest, dict) or manifest.get('version') != 1 or not isinstance(manifest.get('files'), dict): + raise ValueError('unsupported schema') + for section in ('files', 'imports', 'pages', 'tasks', 'declarations'): + if not isinstance(manifest.get(section, {}), dict): + raise ValueError(f'{section} must be an object') + for record in manifest.get('files', {}).values(): + if not isinstance(record, dict) or not isinstance(record.get('source'), list) or len(record['source']) != 3 or not all(isinstance(item, str) for item in record['source']) or not isinstance(record.get('mode'), int) or not isinstance(record.get('sha256'), str): + raise ValueError('invalid file ownership record') + for record in manifest.get('tasks', {}).values(): + if not isinstance(record, dict) or not isinstance(record.get('mapping'), dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in record['mapping'].items()): + raise ValueError('invalid task mapping') + for record in manifest.get('declarations', {}).values(): + if not isinstance(record, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in record.items()): + raise ValueError('invalid companion declaration') + for record in manifest.get('imports', {}).values(): + if not isinstance(record, dict) or not isinstance(record.get('files'), list) or not all(isinstance(item, str) for item in record['files']): + raise ValueError('invalid import record') + return manifest + except (ValueError, AttributeError) as exc: + raise ValueError(f'{label}: invalid import manifest: {exc}') from exc + + +def source_manifest(plan): + path = safe_file(plan.source_root, MANIFEST, 'source manifest') + if not path.exists(): + return {} + data = path.read_bytes() + plan.source_expected[path] = data + return load_manifest(data, path) + + +def declarations(props): + return {key: value for key, value in props.items() + if key in ('logseq-entity', 'entity-tasks', 'source-link') or key.startswith('task-')} + + +def source_properties(plan, page, file): + props, _ = core.parse_properties(file.read_text()) + if props.get('logseq-proxy-url'): + latest = source_manifest(plan).get('declarations', {}).get(page) + if latest is not None: + for key in list(props): + if key in ('logseq-entity', 'entity-tasks', 'source-link') or key.startswith('task-'): + del props[key] + props.update(latest) + return props + + +def task_spec(plan, page, props): + required = ('task-owner', 'task-config-root', 'task-name', 'source-link', 'task-entrypoint', 'task-files') + for key in required: + if not props.get(key): + raise ValueError(f'{page}: missing {key}') + repo = repository(plan.source_root) + scope = props['task-config-root'] + if scope != '.': + relative(scope, f'{page} task-config-root') + entry = relative(props['task-entrypoint'], f'{page} task-entrypoint') + name = props['task-name'] + name_path = PurePosixPath(*name.split(':')) + if not name or '/' in name or '\\' in name or any(not part or part in ('.', '..') for part in name.split(':')): + raise ValueError(f'{page}: invalid task-name {name}') + try: + mapping = json.loads(props['task-files']) + except json.JSONDecodeError as exc: + raise ValueError(f'{page}: task-files must be a JSON object') from exc + if not isinstance(mapping, dict) or not mapping or not all(isinstance(k, str) and isinstance(v, str) for k, v in mapping.items()): + raise ValueError(f'{page}: task-files must map source paths to destination paths') + if mapping.get(str(entry)) != str(PurePosixPath('mise-tasks') / name_path): + raise ValueError(f'{page}: task-files must map the entrypoint to mise-tasks/{name_path}') + imported = source_manifest(plan).get('tasks', {}).get(page, {}) + identity = [repository_identity(repo), plan.source_root.relative_to(repo).as_posix(), props['task-owner'], scope, name] + files = [] + for source, destination in mapping.items(): + dest = relative(destination, f'{page} destination') + if dest.parts[0] != 'mise-tasks' or len(dest.parts) < 2: + raise ValueError(f'{page}: task destinations must be implementation files under mise-tasks: {destination}') + if any(part in ('.git', '.logseq-proxy') for part in dest.parts): + raise ValueError(f'{page}: reserved destination {destination}') + actual_source = imported.get('mapping', {}).get(source, source) + if imported and relative(actual_source, f'{page} imported source').parts[0] != 'mise-tasks': + raise ValueError(f'{page}: imported source must be under mise-tasks') + actual_root = plan.source_root if imported else repo + if any(part in ('.git', '.logseq-proxy') for part in relative(actual_source, f'{page} source').parts): + raise ValueError(f'{page}: reserved implementation source {actual_source}') + file = safe_file(actual_root, actual_source, f'{page} source') + safe_file(plan.destination_root, destination, f'{page} destination') + if not file.is_file(): + raise ValueError(f'{page}: missing implementation file {source}') + mode = stat.S_IMODE(file.stat().st_mode) + if source == str(entry) and not mode & 0o111: + raise ValueError(f'{page}: entrypoint is not executable: {source}') + data = file.read_bytes() + plan.source_expected[file] = data + plan.source_modes[file] = mode + source_relative = file.relative_to(repo).as_posix() + files.append((source_relative, destination, data, mode)) + return identity, files + + +def extend_plan(plan): + """Extend the page plan without writing to either garden.""" + compatible_config(plan.destination_root) + manifest_path = safe_file(plan.destination_root, MANIFEST, 'manifest') + if manifest_path.exists(): + manifest = load_manifest(manifest_path.read_bytes(), manifest_path) + else: + manifest = {'version': 1, 'files': {}, 'imports': {}} + records = manifest['files'] + queue = list(plan.pages) + visited = set() + claims = {} + required_tasks = set() + contents = {} + while queue: + page = queue.pop() + if page in visited: + continue + visited.add(page) + file = core.resolve_page(plan.source_root, page) + if file is None or not file.is_file(): + plan.warnings.append(f'Entity definition {page} has no source file; no companion declaration can be read') + continue + props = source_properties(plan, page, file) + manifest.setdefault('declarations', {})[page] = declarations(props) + for entity in refs(props.get('logseq-entity')): + entity_file = core.resolve_page(plan.source_root, entity) + if entity_file is None or not entity_file.is_file(): + plan.warnings.append(f'Entity definition {entity} has no source file; no companion declaration can be read') + continue + core.add_page(plan, entity) + queue.append(entity) + task_refs = refs(props.get('entity-tasks')) + refs(props.get('task-dependencies')) + for task in task_refs: + task_file = core.resolve_page(plan.source_root, task) + if task_file is None or not task_file.is_file(): + raise ValueError(f'{page}: missing required task reference {task}') + task_props = source_properties(plan, task, task_file) + task_spec(plan, task, task_props) + required_tasks.add(task) + core.add_page(plan, task) + queue.append(task) + if page not in required_tasks and not props.get('task-files'): + if props.get('task-entrypoint'): + raise ValueError(f'{page}: missing task-files') + continue + identity, files = task_spec(plan, page, props) + manifest.setdefault('tasks', {})[page] = {'identity': identity, 'mapping': json.loads(props['task-files'])} + for source, destination, data, mode in files: + source_identity = [identity[0], identity[1], source] + claim = {'source': source_identity, 'sha256': digest(data), 'mode': mode} + if destination in claims and claims[destination]['source'] != source_identity: + raise ValueError(f'Competing source files claim {destination}') + claims[destination] = claim + contents[destination] = data + repo = repository(plan.source_root) + page_records = manifest.setdefault('pages', {}) + for page in plan.pages: + identity = [repository_identity(repo), plan.source_root.relative_to(repo).as_posix(), page] + previous = page_records.get(page) + if previous and previous != identity: + raise ValueError(f'{page}: page already imported from a different source graph') + page_records[page] = identity + for destination, claim in claims.items(): + path = safe_file(plan.destination_root, destination, 'task destination') + previous = records.get(destination) + if previous and previous.get('source') != claim['source']: + raise ValueError(f'{destination}: already claimed by a different source') + if path.exists(): + if not path.is_file(): + raise ValueError(f'{destination}: destination is not a regular file') + if not previous: + raise ValueError(f'{destination}: existing implementation is unowned; move it aside before importing') + if digest(path.read_bytes()) != previous.get('sha256') or stat.S_IMODE(path.stat().st_mode) != previous.get('mode'): + raise ValueError(f'{destination}: imported implementation was edited locally') + elif previous: + raise ValueError(f'{destination}: imported implementation was deleted locally') + core.add_write(plan, destination, contents[destination], mode=claim['mode']) + records[destination] = claim + repo = repository(plan.source_root) + import_key = repository_identity(repo) + '::' + plan.source_root.relative_to(repo).as_posix() + '::' + plan.page + imports = manifest.setdefault('imports', {}) + previous_files = imports.get(import_key, {}).get('files', []) + for destination in previous_files: + if destination not in claims: + plan.warnings.append(f'{destination}: removed upstream; cleanup candidate retained locally') + # The graph path is repository-relative so the manifest stays the same across checkouts. + imports[import_key] = {'source_graph': plan.source_root.relative_to(repo).as_posix(), 'page': plan.page, 'files': sorted(set(previous_files) | set(claims))} + if plan.pages or claims or manifest_path.exists(): + core.add_write(plan, MANIFEST, (json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode(), mode=0o644) diff --git a/gitp-garden/mise-tasks/logseq/entity/proxy/page/lib/core.py b/gitp-garden/mise-tasks/logseq/entity/proxy/page/lib/core.py new file mode 100644 index 0000000..e78feec --- /dev/null +++ b/gitp-garden/mise-tasks/logseq/entity/proxy/page/lib/core.py @@ -0,0 +1,510 @@ +"""Plan and safely apply page proxy imports (standard library only).""" +from __future__ import annotations + +import base64 +from dataclasses import dataclass, field +from datetime import datetime +import fcntl +import json +import os +from pathlib import Path, PurePosixPath +import re +import shutil +import subprocess +import sys +from urllib.parse import parse_qs, quote, unquote, urlparse +from zoneinfo import ZoneInfo + +PROXY_KEYS = ('logseq-proxy-url', 'logseq-proxy-codeforge-url', 'logseq-proxy-last-sync-date') +PROXY_ENTITY = 'Logseq/Entity/Proxy/Page' +# An entity definition lists the properties a proxy's destination garden owns. +DESTINATION_PROPERTIES = 'entity-proxy-destination-properties' +EMBED = re.compile(r'\{\{embed\s+\[\[([^\]]+)\]\]\s*\}\}') +TRANSACTION = '.logseq-proxy-transaction' +PROPERTY = re.compile(r'^([^\s:]+):: (.*?)(?:\r?\n)?$') + + +class SyncError(ValueError): + """A conflict or unsafe import; no writes should be made.""" + + +@dataclass +class Plan: + source_root: Path + destination_root: Path + page: str + writes: dict[str, bytes] = field(default_factory=dict) + warnings: list[str] = field(default_factory=list) + details: list[str] = field(default_factory=list) + expected: dict[str, bytes | None] = field(default_factory=dict) + modes: dict[str, int] = field(default_factory=dict) + pages: set[str] = field(default_factory=set) + codeforge_url: str | None = None + expected_modes: dict[str, int | None] = field(default_factory=dict) + source_expected: dict[Path, bytes] = field(default_factory=dict) + source_modes: dict[Path, int] = field(default_factory=dict) + follow_embeds: bool = False + + +def safe_path(root: Path, relative: str) -> Path: + """Reject traversal and symlinks, including ancestor directories.""" + part = PurePosixPath(relative) + if not relative or part.is_absolute() or '..' in part.parts or '\\' in relative or '\x00' in relative: + raise SyncError(f'Unsafe relative path: {relative}') + root = Path(root).absolute() + for parent in (root, *root.parents): + if parent.is_symlink(): + raise SyncError(f'Symlink path: {parent}') + result = root.joinpath(*part.parts) + current = root + for component in part.parts: + current = current / component + if current.is_symlink(): + raise SyncError(f'Symlink path: {current}') + if result.exists() and not result.is_file(): + raise SyncError(f'Expected a regular file: {result}') + return result + + +def validate_graph(root: Path) -> Path: + root = Path(root).absolute() + # macOS standard temporary directories are system aliases. + if sys.platform == 'darwin' and root.parts[1:2] in (('tmp',), ('var',)): + root = Path('/private').joinpath(*root.parts[1:]) + safe_path(root, 'pages/.proxy-validation') + if not (root / 'pages').is_dir() or not (root / 'logseq').is_dir(): + raise SyncError(f'Graph must contain pages/ and logseq/: {root}') + return root + + +def property_lines(text: str) -> tuple[list[str], str]: + lines = text.splitlines(keepends=True) + stop = 0 + while stop < len(lines) and PROPERTY.match(lines[stop]): + stop += 1 + return lines[:stop], ''.join(lines[stop:]) + + +def parse_properties(text: str) -> tuple[dict[str, str], str]: + lines, body = property_lines(text) + props = {} + for line in lines: + match = PROPERTY.match(line) + key, value = match.groups() + if key in props: + raise SyncError(f'Duplicate property: {key}') + props[key] = value + return props, body + + +def property_key(line: str) -> str: + return PROPERTY.match(line).group(1) + + +def with_proxy_membership(lines: list[str]) -> list[str]: + """Add the Proxy Page entity to logseq-entity, creating the property if needed.""" + marker = f'[[{PROXY_ENTITY}]]' + if not any(property_key(line) == 'logseq-entity' for line in lines): + return [f'logseq-entity:: {marker}\n'] + lines + return [line.rstrip('\r\n') + f', {marker}\n' + if property_key(line) == 'logseq-entity' and marker not in line else line + for line in lines] + + +def destination_owned(plan: Plan, source_props: dict[str, str]) -> set[str]: + """Properties a re-sync keeps from the destination: tags plus each definition's declared keys.""" + keys = {'tags'} + entities = set(re.findall(r'\[\[([^\]]+)\]\]', source_props.get('logseq-entity', ''))) | {PROXY_ENTITY} + for entity in sorted(entities): + definition = resolve_page(plan.source_root, entity) + if not definition.is_file(): + continue + data = definition.read_bytes() + plan.source_expected[definition] = data + props, _ = parse_properties(data.decode('utf-8')) + keys.update(key.strip() for key in props.get(DESTINATION_PROPERTIES, '').split(',') if key.strip()) + return keys + + +def merge_properties(destination: list[str], source: list[str], owned: set[str]) -> tuple[list[str], list[str]]: + """Take source properties on re-sync, keeping destination-owned lines and the destination's order.""" + incoming = {property_key(line): line for line in source if property_key(line) not in PROXY_KEYS} + merged, changes, seen = [], [], set() + for line in destination: + key = property_key(line) + if key in PROXY_KEYS: + continue + seen.add(key) + if key in owned: + merged.append(line) + elif key not in incoming: + changes.append(f'removed {key}') + elif incoming[key].rstrip('\r\n') == line.rstrip('\r\n'): + merged.append(line) + else: + merged.append(incoming[key]) + changes.append(f'updated {key}') + for key, line in incoming.items(): + if key not in seen and key not in owned: + merged.append(line) + changes.append(f'added {key}') + return merged, changes + + +def page_filename(page: str) -> str: + if not page or page.startswith('/') or any(x in ('', '.', '..') for x in page.split('/')) or '\\' in page: + raise SyncError(f'Unsafe page name: {page}') + return quote(page.replace('/', '___'), safe=' _-.,()[]') + '.md' + + +def resolve_page(root: Path, page: str) -> Path: + encoded = page_filename(page) + raw = page.replace('/', '___') + '.md' + candidates = {safe_path(root, 'pages/' + name) for name in (encoded, raw)} + # Logseq escapes reserved punctuation but leaves common Unicode names readable. + for p in (Path(root) / 'pages').glob('*.md'): + if unquote(p.stem).replace('___', '/') == page: + safe_path(root, str(p.relative_to(root))) + candidates.add(p) + existing = [p for p in candidates if p.exists()] + if len(existing) > 1: + raise SyncError(f'Ambiguous page filename for {page}: {existing}') + return existing[0] if existing else safe_path(root, 'pages/' + encoded) + + +def graph_url(root: Path, page: str) -> str: + return f'logseq://graph/{quote(graph_name(root), safe="")}?page={quote(page, safe="")}' + + +def proxy_identity(url: str) -> tuple[str, str]: + parsed = urlparse(url) + values = parse_qs(parsed.query) + if parsed.scheme != 'logseq' or parsed.netloc != 'graph' or len(values.get('page', [])) != 1: + raise SyncError(f'Invalid proxy URL: {url}') + return unquote(parsed.path.lstrip('/')), values['page'][0] + + +def run_local(*args: str, cwd: Path | None = None) -> str: + try: + return subprocess.check_output(args, cwd=cwd, text=True, stderr=subprocess.DEVNULL).strip() + except (OSError, subprocess.CalledProcessError): + return '' + + +def primary_checkout(root: Path) -> Path | None: + worktrees = run_local('git', 'worktree', 'list', '--porcelain', cwd=root) + first = worktrees.splitlines()[0] if worktrees else '' + return Path(first.removeprefix('worktree ')) if first.startswith('worktree ') else None + + +def graph_name(root: Path) -> str: + """Use the primary checkout's graph name while reading a linked worktree.""" + repo = run_local('git', 'rev-parse', '--show-toplevel', cwd=root) + primary = primary_checkout(root) if repo else None + return (primary / root.relative_to(Path(repo))).name if primary else root.name + + +def infer_codeforge(root: Path, source: Path) -> str | None: + repo = run_local('git', 'rev-parse', '--show-toplevel', cwd=root) + remote = run_local('git', 'remote', 'get-url', 'origin', cwd=root) + if not repo or not remote: + return None + if remote.startswith('git@'): + remote = 'https://' + remote[4:].replace(':', '/', 1) + elif remote.startswith('ssh://'): + p = urlparse(remote) + remote = 'https://' + (p.hostname or '') + p.path + parsed_remote = urlparse(remote) + if parsed_remote.scheme != 'https' or not parsed_remote.netloc: + return None + remote = parsed_remote._replace( + netloc=parsed_remote.netloc.rsplit('@', 1)[-1], query='', fragment='' + ).geturl().removesuffix('.git').rstrip('/') + branch = run_local('git', 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD', cwd=root).removeprefix('origin/') or 'main' + primary = primary_checkout(root) + if primary is not None and Path(repo) != primary: + branch = run_local('git', 'symbolic-ref', '--short', 'HEAD', cwd=root) or run_local('git', 'rev-parse', 'HEAD', cwd=root) or branch + relative = source.relative_to(Path(repo)) + marker = '/-/blob/' if 'gitlab' in urlparse(remote).netloc else '/blob/' + return remote + marker + quote(branch, safe='') + '/' + quote(relative.as_posix(), safe='/') + + +def validate_codeforge(url: str, root: Path, source: Path) -> None: + parsed = urlparse(url) + if parsed.username is not None or parsed.password is not None: + raise SyncError('Code forge URLs must omit embedded credentials') + parts = parsed.path.split('/') + if parsed.scheme != 'https' or not parsed.netloc or 'blob' not in parts or unquote(parts[-1]) != source.name or parts[-2:-1] != ['pages']: + raise SyncError(f'Code forge URL names a different source file: {url}') + expected = infer_codeforge(root, source) + if expected: + expected_url = urlparse(expected) + marker = parts.index('blob') + expected_parts = expected_url.path.split('/') + expected_marker = expected_parts.index('blob') + repo = run_local('git', 'rev-parse', '--show-toplevel', cwd=root) + relative = quote(source.relative_to(Path(repo)).as_posix(), safe='/') + if (parsed.netloc != expected_url.netloc or parts[:marker] != expected_parts[:expected_marker] + or not parsed.path.endswith('/' + relative)): + raise SyncError(f'Code forge source ownership conflict: {url}') + + +def resolve_source(destination: Path, page: str, registry: Path | None = None) -> Path: + target = resolve_page(destination, page) + if not target.exists(): + raise SyncError('Initial import requires --source') + props, _ = parse_properties(target.read_text()) + graph, original_page = proxy_identity(props.get('logseq-proxy-url', '')) + if original_page != page: + raise SyncError(f'Proxy names a different page: {original_page}') + forge = props.get('logseq-proxy-codeforge-url') + if forge: + parsed = urlparse(forge) + path = parsed.path.split('/') + try: + marker = path.index('blob') + repo_parts = path[1:marker] + if repo_parts[-1:] == ['-']: + repo_parts.pop() + address = parsed.netloc + '/' + '/'.join(repo_parts) + clones = run_local('ghq', 'list', '--full-path', '--exact', address).splitlines() + if len(clones) != 1: + raise SyncError(f'Expected one ghq clone for {address}; found {len(clones)}') + # pages/ disambiguates a branch containing slashes from the nested graph. + pages_index = path.index('pages', marker + 2) + clone = Path(clones[0]) + candidates = [] + for start in range(marker + 2, pages_index + 1): + relative = '/'.join(unquote(x) for x in path[start:pages_index]) + candidate = clone / relative + if (candidate / 'pages').is_dir() and (candidate / 'logseq').is_dir() and candidate.name == graph: + candidates.append(candidate) + if len(candidates) != 1: + raise SyncError(f'Cannot uniquely resolve graph {graph} in {clone}') + root = validate_graph(candidates[0]) + actual = safe_path(root, 'pages/' + unquote(path[pages_index + 1])) + if actual != resolve_page(root, page): + raise SyncError('Code forge URL names a different source file') + return root + except (ValueError, IndexError) as error: + if isinstance(error, SyncError): + raise + raise SyncError(f'Invalid code forge source URL: {forge}') from error + registry = registry or Path.home() / '.logseq/graphs' + matches = [] + if registry.is_dir(): + for record in registry.iterdir(): + name = record.name + if name.startswith('logseq_local_'): + decoded = name.removeprefix('logseq_local_').removesuffix('.transit').replace('++', '/') + root = Path(decoded) + if root.name == graph and (root / 'pages').is_dir() and (root / 'logseq').is_dir(): + matches.append(root) + matches = list(set(matches)) + if len(matches) != 1: + raise SyncError(f'Expected one registered graph named {graph}; found {len(matches)}; use --source') + return validate_graph(matches[0]) + + +def add_write(plan: Plan, relative: str, data: bytes, mode: int | None = None) -> None: + target = safe_path(plan.destination_root, relative) + if relative in plan.writes and plan.writes[relative] != data: + raise SyncError(f'Conflicting planned writes: {relative}') + if relative not in plan.expected: + plan.expected[relative] = target.read_bytes() if target.exists() else None + plan.expected_modes[relative] = target.stat().st_mode & 0o777 if target.exists() else None + plan.writes[relative] = data + plan.modes[relative] = mode if mode is not None else (plan.expected_modes[relative] or 0o644) + + +def add_page(plan: Plan, page: str) -> None: + if page in plan.pages: + return + source = resolve_page(plan.source_root, page) + if not source.exists(): + raise SyncError(f'Missing source page: {source}') + target = resolve_page(plan.destination_root, page) + src_data = source.read_bytes() + plan.source_expected[source] = src_data + text = src_data.decode('utf-8') + source_props, body = parse_properties(text) + source_lines, _ = property_lines(text) + if plan.codeforge_url and page == plan.page: + validate_codeforge(plan.codeforge_url, plan.source_root, source) + first = not target.exists() + if first: + target = safe_path(plan.destination_root, 'pages/' + source.name) + if not first: + old_text = target.read_bytes().decode('utf-8') + props, _ = parse_properties(old_text) + if 'logseq-proxy-url' not in props: + raise SyncError(f'Page collision: {target} is not a proxy') + if proxy_identity(props['logseq-proxy-url']) != (graph_name(plan.source_root), page): + raise SyncError(f'Proxy source conflict: {target}') + if props.get('logseq-proxy-codeforge-url'): + validate_codeforge(props['logseq-proxy-codeforge-url'], plan.source_root, source) + old_lines, _ = property_lines(old_text) + lines, changes = merge_properties(old_lines, with_proxy_membership(source_lines), destination_owned(plan, source_props)) + else: + lines, changes = with_proxy_membership(source_lines), [] + retained = [line for line in lines if property_key(line) not in PROXY_KEYS] + # A final property without a newline still needs to remain a separate property. + retained = [line if line.endswith('\n') else line + '\n' for line in retained] + metadata = {'logseq-proxy-url': graph_url(plan.source_root, page)} + forge = plan.codeforge_url if page == plan.page and plan.codeforge_url else infer_codeforge(plan.source_root, source) + if forge: + metadata['logseq-proxy-codeforge-url'] = forge + metadata['logseq-proxy-last-sync-date'] = '[[' + datetime.now(ZoneInfo('America/New_York')).date().isoformat() + ']]' + data = ''.join(retained) + ''.join(f'{key}:: {value}\n' for key, value in metadata.items()) + body + relative = str(target.relative_to(plan.destination_root)) + add_write(plan, relative, data.encode()) + plan.pages.add(page) + plan.details.append(f'{"Create" if first else "Re-sync"} {source} -> {target}') + plan.details.extend(f' {key}:: {value}' for key, value in metadata.items()) + plan.details.extend(f' property {change}' for change in changes) + # Capture Markdown link targets, allowing <...> around paths with literal spaces. + assets = re.findall(r'\]\(\s*(?:<([^>]+)>|([^\s)]+))', body) + for a, b in assets: + url = a or b + if not url.startswith('../assets/'): + continue + relative = unquote(url.split('#', 1)[0]).removeprefix('../') + asset = safe_path(plan.source_root, relative) + safe_path(plan.destination_root, relative) + if not asset.exists(): + plan.warnings.append(f'Missing asset: {asset}') + continue + asset_data = asset.read_bytes() + plan.source_expected[asset] = asset_data + add_write(plan, relative, asset_data) + plan.details.append(f'Asset {asset} -> {plan.destination_root / relative}') + if plan.follow_embeds: + for embedded in EMBED.findall(body): + if resolve_page(plan.source_root, embedded).exists(): + add_page(plan, embedded) + else: + plan.warnings.append(f'Missing embedded page: {embedded}') + + +def build_page_plan(source: Path | None, destination: Path, page: str, codeforge_url: str | None = None, + follow_embeds: bool = False) -> Plan: + destination = validate_graph(destination) + source = validate_graph(source if source is not None else resolve_source(destination, page)) + if source == destination: + raise SyncError('Source and destination must be different graphs') + plan = Plan(source, destination, page, codeforge_url=codeforge_url, follow_embeds=follow_embeds) + add_page(plan, page) + return plan + + +def _remove_empty_dirs(root: Path, directories: list[str]) -> None: + for relative in sorted(directories, key=lambda p: len(PurePosixPath(p).parts), reverse=True): + try: + (root / relative).rmdir() + except OSError: + pass + + +def _recover(destination: Path) -> None: + txn = destination / TRANSACTION + if not txn.exists(): + return + if txn.is_symlink(): + raise SyncError(f'Symlink transaction: {txn}') + journal_file = safe_path(destination, TRANSACTION + '/journal.json') + if journal_file.exists(): + journal = json.loads(journal_file.read_text()) + if not isinstance(journal, dict) or not isinstance(journal.get('files'), list) or not isinstance(journal.get('directories'), list): + raise SyncError(f'Invalid recovery journal: {journal_file}') + restored = [] + for item in journal['files']: + if not isinstance(item, dict) or not {'path', 'original', 'mode'} <= item.keys(): + raise SyncError(f'Invalid recovery entry: {journal_file}') + target = safe_path(destination, item['path']) + original = item['original'] + if original is not None: + if not isinstance(item['mode'], int): + raise SyncError(f'Invalid recovery mode: {journal_file}') + original = base64.b64decode(original, validate=True) + restored.append((target, original, item['mode'])) + for relative in journal['directories']: + safe_path(destination, relative + '/.recovery-validation') + for target, original, mode in restored: + if original is None: + target.unlink(missing_ok=True) + else: + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(original) + target.chmod(mode) + _remove_empty_dirs(destination, journal['directories']) + shutil.rmtree(txn) + + +def recover_transaction(destination: Path) -> None: + destination = validate_graph(destination) + lock_fd = os.open(destination, os.O_RDONLY) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX) + _recover(destination) + finally: + os.close(lock_fd) + + +def apply_plan(plan: Plan) -> None: + root = plan.destination_root + lock_fd = os.open(root, os.O_RDONLY) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX) + if (root / TRANSACTION).exists(): + raise SyncError('Interrupted import exists; run --destination --recover, then preview again') + for source, expected in plan.source_expected.items(): + safe_path(source.parent, source.name) + if not source.exists() or source.read_bytes() != expected or (source in plan.source_modes and source.stat().st_mode & 0o777 != plan.source_modes[source]): + raise SyncError(f'Source changed since preview: {source}') + changes = {} + for relative, data in plan.writes.items(): + target = safe_path(root, relative) + actual = target.read_bytes() if target.exists() else None + mode = target.stat().st_mode & 0o777 if target.exists() else None + if actual != plan.expected[relative] or mode != plan.expected_modes[relative]: + raise SyncError(f'Destination changed since preview: {relative}') + if actual != data or mode != plan.modes[relative]: + changes[relative] = data + if not changes: + return + directories = set() + for relative in changes: + parent = (root / relative).parent + while parent != root and not parent.exists(): + directories.add(str(parent.relative_to(root))) + parent = parent.parent + txn = root / TRANSACTION + txn.mkdir() + journal = {'directories': sorted(directories), 'files': [ + {'path': p, 'original': base64.b64encode(plan.expected[p]).decode() if plan.expected[p] is not None else None, 'mode': plan.expected_modes[p]} + for p in changes]} + try: + # Persist rollback information before replacing any destination file. + with (txn / 'journal.json').open('w') as handle: + json.dump(journal, handle) + handle.flush() + os.fsync(handle.fileno()) + staged = [] + for index, (relative, data) in enumerate(changes.items()): + stage = txn / str(index) + with stage.open('wb') as handle: + handle.write(data) + handle.flush() + os.fsync(handle.fileno()) + stage.chmod(plan.modes[relative]) + staged.append((stage, safe_path(root, relative))) + for stage, target in staged: + safe_path(root, str(target.relative_to(root))) + target.parent.mkdir(parents=True, exist_ok=True) + os.replace(stage, target) + except BaseException: + _recover(root) + raise + shutil.rmtree(txn) + finally: + os.close(lock_fd) diff --git a/gitp-garden/mise-tasks/logseq/entity/proxy/page/sync b/gitp-garden/mise-tasks/logseq/entity/proxy/page/sync new file mode 100755 index 0000000..0a9de95 --- /dev/null +++ b/gitp-garden/mise-tasks/logseq/entity/proxy/page/sync @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +#MISE description="Preview or apply Logseq page proxies and their companion tasks" +#MISE tools={python="3.13.16"} +#USAGE flag "--source " help="Source graph root (inferred for existing proxies)" +#USAGE flag "--destination " required=#true help="Destination graph root" +#USAGE flag "--page " help="Exact logical page name" +#USAGE flag "--codeforge-url " help="Source page URL on a code forge" +#USAGE flag "--follow-embeds" help="Also sync pages the page embeds, recursively" +#USAGE flag "--apply" help="Apply the previewed import" +#USAGE flag "--recover" help="Roll back an interrupted import, then exit" +"""Task reference: [[Logseq/Entity/Proxy/Page/mise/Task/sync]].""" +import argparse +import os +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib')) +from core import SyncError, apply_plan, build_page_plan, recover_transaction + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path) + parser.add_argument('--destination', type=Path, required=True) + parser.add_argument('--page') + parser.add_argument('--codeforge-url') + parser.add_argument('--follow-embeds', action='store_true') + parser.add_argument('--apply', action='store_true') + parser.add_argument('--recover', action='store_true') + args = list(sys.argv[1:]) + # Mise usage supplies parsed options through environment variables. + if not args and 'usage_destination' in os.environ: + for name in ('source', 'destination', 'page', 'codeforge_url'): + if os.environ.get('usage_' + name): + args.extend(['--' + name.replace('_', '-'), os.environ['usage_' + name]]) + for name in ('follow_embeds', 'apply', 'recover'): + if os.environ.get('usage_' + name) == 'true': + args.append('--' + name.replace('_', '-')) + options = parser.parse_args(args) + try: + if options.recover: + recover_transaction(options.destination) + print('Interrupted import rolled back; run a fresh preview.') + return 0 + if not options.page: + parser.error('--page is required unless --recover is used') + plan = build_page_plan(options.source, options.destination, options.page, options.codeforge_url, + options.follow_embeds) + try: + import companions + except ModuleNotFoundError as error: + if error.name != 'companions': + raise + raise SyncError('Missing companion importer lib/companions.py; restore the task implementation before syncing') from error + else: + companions.extend_plan(plan) + print('\n'.join(plan.details)) + for warning in plan.warnings: + print('Warning: ' + warning) + for relative, data in sorted(plan.writes.items()): + status = 'unchanged' if plan.expected[relative] == data else 'write' + print(f'{status}: {relative}') + if options.apply: + apply_plan(plan) + print('Applied.') + else: + print('Preview only. Use --apply to write these changes.') + except (ValueError, OSError) as error: + print(f'Cannot sync: {error}', file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/gitp-garden/pages/Acolyte.md b/gitp-garden/pages/Acolyte.md deleted file mode 100644 index 709d940..0000000 --- a/gitp-garden/pages/Acolyte.md +++ /dev/null @@ -1,20 +0,0 @@ -# Python package `gitp-acolyte` documentation -- assists in conducting ceremonies and making offerings -- ## [[Acolyte/Reference]] - [[Reference]] documentation - - [[Acolyte/How To/acolyte-cli]] a command line application implemented in [[Acolyte/Reference/gitp_acolyte]] and its sub-packages - - [[Acolyte/Reference/gitp_acolyte]] **python package root** - - [[Acolyte/Reference/gitp_acolyte/ceremonial]] **tools for creating [[Ceremonies]]** - - [[Acolyte/Reference/gitp_acolyte/ceremonial/grimoire]] **spellbook for conducting [[Ceremonies]]** - - move files from Zoom H5 to Ableton project dir - - create Ableton Project dir - - audio processing of episode - - transcription - - creating episode yaml - - [[Acolyte/Reference/gitp_acolyte/ceremonial/offering]] **tools for converting conducted [[Ceremonies]] into offerings to the spirits** - - convert episode yml into RSS XML -- ## [[Acolyte/How To]] - [[How To Guide]]s for using `gitp-acolyte` - - -- ## [[Acolyte/Tutorial]] - [[Tutorial]]s for learning `gitp-acolyte` - - -- ## [[Acolyte/Concept]] - [[Conceptual Guide]]s for understanding `gitp-acolyte` - - [[Acolyte/Concept/acolyte-cli Overview]] \ No newline at end of file diff --git a/gitp-garden/pages/Acolyte___Reference.md b/gitp-garden/pages/Acolyte___Reference.md deleted file mode 100644 index 6044fd9..0000000 --- a/gitp-garden/pages/Acolyte___Reference.md +++ /dev/null @@ -1,2 +0,0 @@ -# Reference Docs for `gitp-acolyte` -- \ No newline at end of file diff --git a/gitp-garden/pages/Ceremony.md b/gitp-garden/pages/Ceremony.md deleted file mode 100644 index f5feb76..0000000 --- a/gitp-garden/pages/Ceremony.md +++ /dev/null @@ -1 +0,0 @@ -alias:: [[Ceremonies]] diff --git a/gitp-garden/pages/Ceremony___2024___11___19.md b/gitp-garden/pages/Ceremony___2024___11___19.md deleted file mode 100644 index 9982b17..0000000 --- a/gitp-garden/pages/Ceremony___2024___11___19.md +++ /dev/null @@ -1,12 +0,0 @@ -public:: true -date-created:: [[2024-11-19 Tue]] -type:: [[Podcast/Episode]] - -- # Ceremony - 2024-11-19 - - ![furry-onesie-rainbow-goblin-2.gif](../assets/Ceremony/2024/11/19/furry-onesie-rainbow-goblin-2.gif) - - ![GitP.2024.11.19.mix.128kbps_CBR.mp3](../assets/Ceremony/2024/11/19/GitP.2024.11.19.mix.128kbps_CBR.mp3) -- ## Files - - [[Microfreak/Patch]] files - - [Download GitP.2024.11.18.A.mfpatch.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/11/19/GitP.2024.11.18.A.mfpatch.mfpz) - - [Download GitP.2024.11.18.B.mfpatch.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/11/19/GitP.2024.11.18.B.mfpatch.mfpz) - - [Download GitP.2024.11.18.C.mfpatch.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/11/19/GitP.2024.11.18.C.mfpatch.mfpz) \ No newline at end of file diff --git a/gitp-garden/pages/Ceremony___2024___12___04.md b/gitp-garden/pages/Ceremony___2024___12___04.md deleted file mode 100644 index c3f86a4..0000000 --- a/gitp-garden/pages/Ceremony___2024___12___04.md +++ /dev/null @@ -1,15 +0,0 @@ -public:: true -date-created:: [[2024-12-04 Wed]] -type:: [[Podcast/Episode]] - -- # Ceremony - 2024-12-04 - - ![furry-onesie-rainbow-goblin.gif](../assets/Ceremony/2024/12/04/furry-onesie-rainbow-goblin.gif) - - ![GitP.2024.12.04.mix.128kbps_CBR.mp3](../assets/Ceremony/2024/12/04/GitP.2024.12.04.mix.128kbps_CBR.mp3) -- ## Files - - [[Microfreak/Patch]] files - - [Download GitP.2024.12.04.A.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GitP.2024.12.04.A.mfpz) - - [Download GitP.2024.12.04.B.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GitP.2024.12.04.B.mfpz) - - [Download GitP.2024.12.04.C.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GitP.2024.12.04.C.mfpz) - - [Download GitP.2024.12.04.D.mfpz](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GitP.2024.12.04.D.mfpz) - - [[Microfreak/MIDI]] files - - [Download GitP.2024.12.04.microfreak.mid](https://raw.githubusercontent.com/codekiln/gitpa/main/assets/Ceremony/2024/12/04/GitP.2024.12.04.microfreak.mid) \ No newline at end of file diff --git a/gitp-garden/pages/Ceremony___2026___09___24.md b/gitp-garden/pages/Ceremony___2026___09___24.md deleted file mode 100644 index 313d2ca..0000000 --- a/gitp-garden/pages/Ceremony___2026___09___24.md +++ /dev/null @@ -1,6 +0,0 @@ -public:: false -date-created:: [[2026-09-24 Thu]] -type:: [[Podcast/Episode]] -- # GitP.26.09.24 - - A MicroFreak and Launchpad session beginning with two-operator FM on an initialized patch. - - ![GitP.26.09.24 — episode recording](https://f005.backblazeb2.com/file/logseq-encode-garden/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full.mp3) diff --git a/gitp-garden/pages/Ceremony___2026___09___25.md b/gitp-garden/pages/Ceremony___2026___09___25.md deleted file mode 100644 index df83242..0000000 --- a/gitp-garden/pages/Ceremony___2026___09___25.md +++ /dev/null @@ -1,7 +0,0 @@ -public:: true -date-created:: [[2026-09-25 Fri]] -type:: [[Podcast/Episode]] - -- # GitP.26.09.25 - - MicroFreak SAWX and WaveUser preset 1910, with a Novation Launchpad sequence. - - [Listen to GitP.26.09.25](https://f005.backblazeb2.com/file/logseq-encode-garden/gitpa/episodes/2026-09-25/GitP.26.09.25.mp3) diff --git a/gitp-garden/pages/Ghost in the Patch.md b/gitp-garden/pages/Ghost in the Patch.md index 4249962..64e6a32 100644 --- a/gitp-garden/pages/Ghost in the Patch.md +++ b/gitp-garden/pages/Ghost in the Patch.md @@ -1,17 +1,26 @@ public:: true - Acolytes of the [[Ghostpatch Guild]], tinkering with [[Etherial Receivers]] such as the [[Microfreak]], tuning into the signals of the [[Sound Spirits]] of the Misty Soundwood. Ghosts in the garden, growing signal *awareness*. -- [Subscribe to the podcast RSS feed](https://codekiln.github.io/gitpa/rss.xml) +- Subscribe to the podcast RSS feed at https://codekiln.github.io/gitpa/rss.xml - ![gitp_logo_raw_fly.JPG](../assets/gitp/logo/gitp_logo_raw_fly.JPG){:height 778, :width 770} - query-table:: false query-properties:: [:page] - query-sort-by:: page - query-sort-desc:: false #+BEGIN_QUERY {:title [:h2 "Recent Ceremonies"] - :query [:find (pull ?b [*]) + :query [:find (pull ?p [*]) :where - (property ?b :type "Podcast/Episode")] - :table-view? true + [?p :block/name] + [?p :block/properties ?props] + [(get ?props :logseq-entity) ?entities] + [(contains? ?entities "Logseq/Entity/Podcast/Episode")] + [(get ?props :public) ?public] + [(= ?public true)]] + :result-transform (fn [rows] (sort-by (fn [row] (get-in row [:block/properties :podcast-published-at])) (fn [a b] (compare b a)) rows)) + :view (fn [rows] + (into ["ul"] + (map (fn [row] + ["li" ["a" {:href (str "#/page/" (clojure.string/replace (:block/name row) "/" "%2F"))} + (:block/original-name row)]]) rows))) + :table-view? false } #+END_QUERY diff --git a/gitp-garden/pages/GitP___A___Session___24___11___19-Tue.md b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue.md new file mode 100644 index 0000000..981006e --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue.md @@ -0,0 +1,17 @@ +public:: true +podcast-guid:: gitpa20241119 +podcast-published-at:: 2026-10-04T14:46:19-04:00 +created-by:: [[Person/codekiln]] +date-created:: [[2024-11-19 Tue]] +logseq-entity:: [[Logseq/Entity/Podcast/Episode]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F11%2F19-Tue +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F197-listener-episode-pages/pages/GitP___A___Session___24___11___19-Tue.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.11.19 + - A MicroFreak session recorded on November 19, 2024. + - ![GitP.24.11.19 artwork](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Artwork___Overview.gif) + - ![GitP.24.11.19 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Synth___Full.mp3) + - ## MicroFreak patches + - [Download MicroFreak preset A](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Preset___A.mfpz) + - [Download MicroFreak preset B](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Preset___B.mfpz) + - [Download MicroFreak preset C](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Preset___C.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Artwork___Overview___gif.md b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Artwork___Overview___gif.md new file mode 100644 index 0000000..7a756bb --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Artwork___Overview___gif.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F11%2F19-Tue%2FAsset%2FArtwork%2FOverview%2Fgif +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___11___19-Tue___Asset___Artwork___Overview___gif.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.11.19 artwork + - ![GitP.24.11.19 artwork](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Artwork___Overview.gif) diff --git a/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___A___mfpz.md b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___A___mfpz.md new file mode 100644 index 0000000..86c335f --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___A___mfpz.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F11%2F19-Tue%2FAsset%2FPreset%2FA%2Fmfpz +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___A___mfpz.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.11.19 preset A + - [Download MicroFreak preset A](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Preset___A.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___B___mfpz.md b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___B___mfpz.md new file mode 100644 index 0000000..939c145 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___B___mfpz.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F11%2F19-Tue%2FAsset%2FPreset%2FB%2Fmfpz +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___B___mfpz.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.11.19 preset B + - [Download MicroFreak preset B](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Preset___B.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___C___mfpz.md b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___C___mfpz.md new file mode 100644 index 0000000..c7b4aa7 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___C___mfpz.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F11%2F19-Tue%2FAsset%2FPreset%2FC%2Fmfpz +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___11___19-Tue___Asset___Preset___C___mfpz.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.11.19 preset C + - [Download MicroFreak preset C](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Preset___C.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Synth___Full___mp3.md b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Synth___Full___mp3.md new file mode 100644 index 0000000..746dca9 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___11___19-Tue___Asset___Synth___Full___mp3.md @@ -0,0 +1,7 @@ +duration-seconds:: 422.752313 +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F11%2F19-Tue%2FAsset%2FSynth%2FFull%2Fmp3 +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___11___19-Tue___Asset___Synth___Full___mp3.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.11.19 MP3 + - ![GitP.24.11.19 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___11___19-Tue___Asset___Synth___Full.mp3) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed.md new file mode 100644 index 0000000..bfd38ed --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed.md @@ -0,0 +1,19 @@ +public:: true +podcast-guid:: gitpa20241204 +podcast-published-at:: 2026-10-04T14:46:20-04:00 +created-by:: [[Person/codekiln]] +date-created:: [[2024-12-04 Wed]] +logseq-entity:: [[Logseq/Entity/Podcast/Episode]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F197-listener-episode-pages/pages/GitP___A___Session___24___12___04-Wed.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 + - A MicroFreak session recorded on December 4, 2024. + - ![GitP.24.12.04 artwork](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Artwork___Overview.gif) + - ![GitP.24.12.04 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Synth___Full.mp3) + - ## MicroFreak patches + - [Download MicroFreak preset A](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___A.mfpz) + - [Download MicroFreak preset B](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___B.mfpz) + - [Download MicroFreak preset C](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___C.mfpz) + - [Download MicroFreak preset D](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___D.mfpz) + - [Download MicroFreak MIDI](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___MIDI___Microfreak.mid) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Artwork___Overview___gif.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Artwork___Overview___gif.md new file mode 100644 index 0000000..fd201a3 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Artwork___Overview___gif.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed%2FAsset%2FArtwork%2FOverview%2Fgif +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___12___04-Wed___Asset___Artwork___Overview___gif.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 artwork + - ![GitP.24.12.04 artwork](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Artwork___Overview.gif) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___MIDI___Microfreak___mid.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___MIDI___Microfreak___mid.md new file mode 100644 index 0000000..65e67a5 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___MIDI___Microfreak___mid.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed%2FAsset%2FMIDI%2FMicrofreak%2Fmid +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___12___04-Wed___Asset___MIDI___Microfreak___mid.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 MicroFreak MIDI + - [Download MicroFreak MIDI](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___MIDI___Microfreak.mid) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___A___mfpz.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___A___mfpz.md new file mode 100644 index 0000000..a00e432 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___A___mfpz.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed%2FAsset%2FPreset%2FA%2Fmfpz +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___A___mfpz.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 preset A + - [Download MicroFreak preset A](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___A.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___B___mfpz.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___B___mfpz.md new file mode 100644 index 0000000..b8face8 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___B___mfpz.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed%2FAsset%2FPreset%2FB%2Fmfpz +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___B___mfpz.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 preset B + - [Download MicroFreak preset B](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___B.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___C___mfpz.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___C___mfpz.md new file mode 100644 index 0000000..37706bc --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___C___mfpz.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed%2FAsset%2FPreset%2FC%2Fmfpz +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___C___mfpz.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 preset C + - [Download MicroFreak preset C](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___C.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___D___mfpz.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___D___mfpz.md new file mode 100644 index 0000000..32891bd --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___D___mfpz.md @@ -0,0 +1,6 @@ +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed%2FAsset%2FPreset%2FD%2Fmfpz +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___12___04-Wed___Asset___Preset___D___mfpz.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 preset D + - [Download MicroFreak preset D](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Preset___D.mfpz) diff --git a/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Synth___Full___mp3.md b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Synth___Full___mp3.md new file mode 100644 index 0000000..1d6f316 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___24___12___04-Wed___Asset___Synth___Full___mp3.md @@ -0,0 +1,7 @@ +duration-seconds:: 2872.065669 +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F24%2F12%2F04-Wed%2FAsset%2FSynth%2FFull%2Fmp3 +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___24___12___04-Wed___Asset___Synth___Full___mp3.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.24.12.04 MP3 + - ![GitP.24.12.04 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___24___12___04-Wed___Asset___Synth___Full.mp3) diff --git a/gitp-garden/pages/GitP___A___Session___26___09___24-Thu.md b/gitp-garden/pages/GitP___A___Session___26___09___24-Thu.md new file mode 100644 index 0000000..a1a3ea9 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___26___09___24-Thu.md @@ -0,0 +1,17 @@ +public:: true +podcast-guid:: gitpa20260924 +podcast-published-at:: 2026-10-04T14:22:23-04:00 +created-by:: [[Person/codekiln]] +date-created:: [[2026-09-24 Thu]] +logseq-entity:: [[Logseq/Entity/Podcast/Episode]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F26%2F09%2F24-Thu +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F197-listener-episode-pages/pages/GitP___A___Session___26___09___24-Thu.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.26.09.24 + - A MicroFreak and Launchpad session beginning with two-operator FM on an initialized patch. + - ![GitP.26.09.24 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full.mp3) + - ## Notes + collapsed:: true + - [[Making/Music/Log/26/09/24 Thu - GitP Reboot with Novation Launchpad]] + - Ableton tracks: commentary, microfreak, Mixed Output all, and Microfreak Midi all. + - Ableton MIDI clips contain no program-change values identifying presets. diff --git a/gitp-garden/pages/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full___mp3.md b/gitp-garden/pages/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full___mp3.md new file mode 100644 index 0000000..7db7024 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full___mp3.md @@ -0,0 +1,7 @@ +duration-seconds:: 757.761066 +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F26%2F09%2F24-Thu%2FAsset%2FSynth%2FFull%2Fmp3 +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full___mp3.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.26.09.24 MP3 + - ![GitP.26.09.24 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full.mp3) diff --git a/gitp-garden/pages/GitP___A___Session___26___09___25-Fri.md b/gitp-garden/pages/GitP___A___Session___26___09___25-Fri.md new file mode 100644 index 0000000..8426db3 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___26___09___25-Fri.md @@ -0,0 +1,15 @@ +public:: true +podcast-guid:: gitpa20260925 +podcast-published-at:: 2026-10-03T12:00:00-04:00 +created-by:: [[Person/codekiln]] +date-created:: [[2026-09-25 Fri]] +logseq-entity:: [[Logseq/Entity/Podcast/Episode]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F26%2F09%2F25-Fri +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F197-listener-episode-pages/pages/GitP___A___Session___26___09___25-Fri.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.26.09.25 + - MicroFreak SAWX and WaveUser preset 1910, with a Novation Launchpad sequence. + - ![GitP.26.09.25 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___26___09___25-Fri___Asset___Synth___Full.mp3) + - ## Notes + collapsed:: true + - [[Music/Composition/Log/26/09/25 Fri]] diff --git a/gitp-garden/pages/GitP___A___Session___26___09___25-Fri___Asset___Synth___Full___mp3.md b/gitp-garden/pages/GitP___A___Session___26___09___25-Fri___Asset___Synth___Full___mp3.md new file mode 100644 index 0000000..2ccd0e5 --- /dev/null +++ b/gitp-garden/pages/GitP___A___Session___26___09___25-Fri___Asset___Synth___Full___mp3.md @@ -0,0 +1,7 @@ +duration-seconds:: 982.009977 +logseq-entity:: [[Logseq/Entity/Asset/B2]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=GitP%2FA%2FSession%2F26%2F09%2F25-Fri%2FAsset%2FSynth%2FFull%2Fmp3 +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/GitP___A___Session___26___09___25-Fri___Asset___Synth___Full___mp3.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # GitP.26.09.25 MP3 + - ![GitP.26.09.25 — episode recording](https://s3.us-east-005.backblazeb2.com/logseq-encode-garden/GitP___A___Session___26___09___25-Fri___Asset___Synth___Full.mp3) diff --git a/gitp-garden/pages/Logseq___Entity___Asset___B2.md b/gitp-garden/pages/Logseq___Entity___Asset___B2.md new file mode 100644 index 0000000..4ce3e9b --- /dev/null +++ b/gitp-garden/pages/Logseq___Entity___Asset___B2.md @@ -0,0 +1,16 @@ +logseq-entity:: [[Logseq/Entity/Definition]], [[Logseq/Entity/Proxy/Page]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=Logseq%2FEntity%2FAsset%2FB2 +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/main/pages/Logseq___Entity___Asset___B2.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # Backblaze B2 Asset + - In this garden, **Backblaze B2 Asset** pages represent files stored in Backblaze B2, following [[Logseq/Entity/Asset]] for filenames, ownership, and page contents. + - ## Storage location + - Upload the page-derived filename at the root of the garden's configured B2 bucket. Its optional local copy uses `assets/.remote/`. + - For `Course/Asset/Diagram/Overview/png`, the filename is `Course___Asset___Diagram___Overview.png` and the local copy is `assets/.remote/Course___Asset___Diagram___Overview.png`. + - The remote URL has the shape `https:///file//Course___Asset___Diagram___Overview.png`. The garden's B2 configuration supplies the host and bucket; URL encoding preserves the filename. + - ## Frontmatter and body + - `logseq-entity:: [[Logseq/Entity/Asset/B2]]` identifies the file as a B2 asset. Shared asset frontmatter and embedded media or download links follow [[Logseq/Entity/Asset]]. + - The body uses the usable remote URL. The URL already identifies the bucket and filename; separate properties would repeat them. + - ## Uploading and backing up + - Uploading makes a file available at its remote location. Publication through a website or podcast feed is a separate step in that site's workflow. + - DVC maintains checksums and restoration metadata separately. A DVC push backs up a tracked file; a named B2 upload supplies the URL used by its asset page. diff --git a/gitp-garden/pages/Logseq___Entity___Definition.md b/gitp-garden/pages/Logseq___Entity___Definition.md new file mode 100644 index 0000000..60610ba --- /dev/null +++ b/gitp-garden/pages/Logseq___Entity___Definition.md @@ -0,0 +1,38 @@ +logseq-entity:: [[Logseq/Entity/Definition]], [[Logseq/Entity/Proxy/Page]] +see-also:: [[Logseq/Entity/Definition/Discussion]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=Logseq%2FEntity%2FDefinition +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F189-proxy-worktree-identity/pages/Logseq___Entity___Definition.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # Entity Definition + - Each Entity Definition page describes a type of real-world entity that may be tracked in a page [[Knowledge Garden]] page. Entity here means something roughly analagous + - ## [[Logseq/Entity/Definition/Discussion]] + - , uniquely modeled in each garden are. + - the garden tracks — each describes how to recognize, name, deduplicate, and shape instances of one kind of entity. + - ## Examples in this garden + - [[Logseq/Entity/Person]], [[Logseq/Entity/Software/Project]], [[Logseq/Entity/Book]], and the other pages under the `Logseq/Entity/` namespace. + - ## What counts as an Entity Definition + - A page under the `Logseq/Entity/` namespace whose job is to define one entity type: what counts as an instance, what does not, how to name and deduplicate it, and what shape its instance pages take. + - Not an Entity Definition: an instance page (an actual person, project, book, …), or [[Logseq/Entity]] itself, which is the general conceptual model rather than a single type. + - ## Page shape + - **Frontmatter** — `logseq-entity:: [[Logseq/Entity/Definition]]` marks the page as an entity definition, so this page collects a backlink to every entity type. Add `alias::` for plural or contextual forms when useful. Never alter `tags::`. + - **H1** — a single bullet-wrapped heading in human-friendly Title Case, with abbreviations expanded and no namespace path: `- # Software Plugin`, `- # Security Vulnerability`, `- # CLI Command`. The page's namespace already carries it. + - **First line** — one sentence directly under the H1 in the form `In this garden, **** pages model .` These first lines read together as a self-describing index of the garden's entity types. + - **Body sections** — describe what is specific to recognizing, naming, deduplicating, and shaping that type. Adapt to the type; short definitions do not need every section. Common sections: + - what counts as an instance, and what does not + - naming and links + - finding and deduplicating + - frontmatter expectations + - page shape + - relationships to other types + - examples + - Keep shared conventions on [[Logseq/Frontmatter]] and [[Logseq/Pref]] rather than repeating them on each type page. + - ## Companion tasks + - `entity-tasks::` declares the [[Logseq/Entity/Mise/Task]] reference pages needed to work with instances of this entity. The property is a comma-separated list of wikilinks in the definition's frontmatter; an absent property declares no companion tasks. + - [[Logseq/Entity/Proxy/Page/mise/Task/sync]] follows an instance's `logseq-entity::` links, imports its entity definitions, and follows each definition's `entity-tasks::` links. Task references declare their executable files and required task dependencies through the companion-file contract on [[Logseq/Entity/Mise/Task]]. + - Shared entity definitions and task dependencies are visited once per sync. Cyclic relationships are allowed and terminate after their pages have been visited. + - A referenced entity definition with no source file is reported as a logical page. A declared companion task or task dependency requires a source reference page and its mapped implementation files; missing task content stops application. + - ## Properties a proxy's destination owns + - `entity-proxy-destination-properties::` names, as a comma-separated list of property keys, the properties on an instance that belong to the garden holding a [[Logseq/Entity/Proxy/Page]] of it rather than to the source page. Re-syncing that proxy keeps the destination's values for them. An absent property names none. + - For example, [[Logseq/Entity/Podcast/Episode]] names `podcast-guid` and `podcast-published-at`, which the publishing garden records. + - ## Relationship to [[Logseq/Entity]] + - [[Logseq/Entity]] is the general conceptual model — what an entity is and how any page is marked as one. This page is the entity type whose instances are the definition pages themselves, so the garden's set of entity types is queryable through its backlinks rather than kept as a hand-maintained list. \ No newline at end of file diff --git a/gitp-garden/pages/Logseq___Entity___Mise___Task.md b/gitp-garden/pages/Logseq___Entity___Mise___Task.md new file mode 100644 index 0000000..8b3ea2a --- /dev/null +++ b/gitp-garden/pages/Logseq___Entity___Mise___Task.md @@ -0,0 +1,49 @@ +logseq-entity:: [[Logseq/Entity/Definition]], [[Logseq/Entity/Proxy/Page]] +see-also:: [[mise/Task]], [[Logseq/Entity/CLI/Command]], [[Logseq/Entity/Diataxis/How To]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=Logseq%2FEntity%2FMise%2FTask +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F189-proxy-worktree-identity/pages/Logseq___Entity___Mise___Task.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # Mise Task + - In this garden, **Mise Task** pages model named jobs supplied by a repository or configuration scope and invoked through [[mise]]. + - ## Identity + - A task is identified by its owning repository or configuration collection, config root, and exact task name. Identical names in separate repositories or config roots identify separate tasks. + - The task definition can be an executable file or a TOML task. A helper module, package, fixture, or test file becomes part of a task's implementation rather than a separate task entity. + - Built-in mise subcommands such as `mise run` are [[Logseq/Entity/CLI/Command]] instances. Jobs invoked through them are Mise Task instances. + - ## Naming and links + - Task reference pages belong to the topic served by the job: `Topic/mise/Task/`. A namespace prefix already expressed by the topic can be omitted from the page name while remaining in `task-name::`. + - For example, `logseq:entity:proxy:page:sync` lives at [[Logseq/Entity/Proxy/Page/mise/Task/sync]]: the topic already expresses `logseq:entity:proxy:page`. Repository ownership remains explicit in frontmatter. + - If the topic contains distinct implementations of the same task name, add a repository or config-scope segment before the task path. Displayed invocation text retains the exact runnable name. + - ## Frontmatter + - `logseq-entity::` marks the page as a Mise Task instance. + - `task-owner::` links the owning repository or configuration collection. + - `task-config-root::` gives the config directory relative to its repository; `.` denotes the repository root. + - `task-name::` records the exact local task name, preserving colons and spelling. + - `source-link::` links the executable file or TOML definition. A review branch or commit link identifies an implementation under review; the repository's default branch identifies the maintained task after it lands. + - Shared page attributes follow [[Logseq/Frontmatter]]. + - ## Companion-file contract + - A portable file task declares `task-entrypoint::`, `task-files::`, and any `task-dependencies::` in its reference page's frontmatter. These properties let [[Logseq/Entity/Proxy/Page/mise/Task/sync]] bring the task's implementation into another garden. + - `task-entrypoint::` is the executable file's path relative to the source repository root, using forward slashes. + - `task-files::` is a single-line JSON object mapping each source repository-relative path to its destination graph-relative path. The object lists the entrypoint and every helper, package file, or isolated configuration file needed at runtime. Destination values remain under `mise-tasks/`. Each path is explicit; wildcards have no meaning. + - The entrypoint maps to `mise-tasks/`. For example, `task-name:: logseq:entity:proxy:page:sync` maps to `mise-tasks/logseq/entity/proxy/page/sync`. The imported entrypoint retains its executable mode. + - Paths are relative, remain inside their respective roots, and identify regular files. Parent traversal and absolute paths are invalid. A source graph nested inside a repository still uses repository-relative source keys; destination values start at the destination graph root. + - `task-config-root::` retains the source task's configuration identity. Runtime tools are declared in the file task's mise metadata. A shared runtime configuration or package is transferable when its required files are explicitly mapped under `mise-tasks/`. Runtime file lookup is relative to the imported entrypoint or its helpers, so the task works after relocation. The destination garden supplies its root mise configuration. + - `task-dependencies::` is an optional comma-separated list of wikilinks to required Mise Task reference pages. Each dependency supplies its own file mapping, and dependencies are followed recursively with a visited set. A missing task page, invalid declaration, or missing mapped file stops application before writes. + - The imported task reference retains `task-owner::`, `task-config-root::`, `task-name::`, and `source-link::` from its source identity. Proxy metadata and the destination manifest record where the imported copy came from. + - Destination paths shared by tasks must describe the same source file. Conflicting sources claiming one destination path stop application. Previously imported implementation files update when their destination content still matches the last import; a local edit stops application and appears in the preview. + - Files removed from the source mapping are reported as cleanup candidates. Re-sync keeps those destination files for review. + - ## Reference content + - A single H1 names the job in plain language. Its first child states what the task accomplishes and when it is useful. + - **Invocation** gives the working directory or repository, the exact command, and one realistic example. Monorepo tasks show their project-qualified invocation. + - **Inputs and outputs** describe required files or arguments, created files and pages, and the result a successful run establishes. + - **Side effects** describes local writes, overwrites, deletion, uploads, and publication when present. Read-only tasks can state their read-only behavior briefly. + - **Dependencies and access** explains tools supplied by mise, Python dependencies supplied by the package manager, platform limits, and credential sources. Version numbers and full argument listings remain in the executable definition, package metadata, and generated help. + - **Failure and recovery** names the failures a reader is likely to meet and the action that resolves each one. + - **Source and help** links the implementation and provides the task's `--help` invocation. Material behavior changes are reflected in the reference page. + - A short task can combine adjacent sections. A task with substantial side effects gives those effects their own section. + - ## How-to pages + - Task pages describe one job. [[Logseq/Entity/Diataxis/How To]] pages describe a goal and may combine several jobs, editorial decisions, and manual checks. + - A how-to links its task reference pages; each task page links relevant how-tos through `see-also::`. The task reference holds the interface and side effects, while the how-to holds the sequence of actions. + - Examples use reusable input paths. Session results and release-specific evidence live on the relevant session pages. + - ## Finding and deduplicating + - Compare `task-owner::`, `task-config-root::`, and `task-name::` before creating a task page. The source path supports the match but can change during a refactor. + - An alias or monorepo path alias that invokes the same job belongs in the existing task reference. A task rename or implementation move updates the existing reference when the job retains its identity. diff --git a/gitp-garden/pages/Logseq___Entity___Podcast___Episode.md b/gitp-garden/pages/Logseq___Entity___Podcast___Episode.md new file mode 100644 index 0000000..b46a769 --- /dev/null +++ b/gitp-garden/pages/Logseq___Entity___Podcast___Episode.md @@ -0,0 +1,59 @@ +logseq-entity:: [[Logseq/Entity/Definition]], [[Logseq/Entity/Proxy/Page]] +entity-proxy-destination-properties:: podcast-guid, podcast-published-at +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=Logseq%2FEntity%2FPodcast%2FEpisode +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F197-listener-episode-pages/pages/Logseq___Entity___Podcast___Episode.md +logseq-proxy-last-sync-date:: [[2026-10-06]] + +- # Podcast Episode + - In this garden, **Podcast Episode** pages model one episode of a [[Logseq/Entity/Podcast]], captured as listening notes or a recording prepared for publication. + - ## What counts as a Podcast Episode + - One specific episode of a show, usually with a title, a host, an air date, and often a URL of its own. + - Not Podcast Episodes: the show itself, which is a [[Logseq/Entity/Podcast]]; the host's person hub; a guest's person hub; a [[Logseq/Entity/Quote]] pulled from an episode; a [[Logseq/Entity/Concept]] page that synthesizes ideas an episode discussed. + - ## Relationship to the show + - An episode is part of one show. Its page carries the Episode marker, and the show's page carries the Podcast marker. + - The episode page files under its show's namespace, and its `created-by::` names the show's host. + - ## Naming and links + - Episode pages live under the show's `Pod` namespace, dated: `Person//Pod// `, or `/Pod// ` for a show filed under its own name. [[Logseq/Entity/Podcast]] says where the show page goes. + - On disk that is `pages/Person______Pod______ .md`. + - `` is the two-digit year and `` the two-digit month of the episode's air date. + - A producer-owned episode recorded in a music session may use that session's dated path: `/A/Session///`. The session page holds the episode description, recording, and notes. For example, [[GitP/A/Session/26/09/24-Thu]]. + - An episode that is also part of a named sequence is both a Podcast Episode and a [[Logseq/Entity/Series]] instance. Its path then adds `Series/` after `Pod`, as [[Logseq/Entity/Series]] describes. + - Pages filed before this definition existed use either `Pod` or `Podcast` as the namespace segment, and some put a `/` after the month instead of a space. New listening-note pages use `Pod` and the space. Leave existing pages as filed unless the author asks for a migration. + - ## Finding and deduplicating + - Search in order: the episode URL; the YouTube video ID when there is a recording; the exact episode title; distinctive title words under `Person/*/Pod*` and `*/Pod*`; then host plus a distinctive phrase. Classify as existing, similar, new, or blocked. + - One page per episode. + - ## Creator relationship + - Set **`created-by::`** to the host's person page, which is the page the episode namespace already sits under. + - Guests are linked from the body; `created-by::` names only the host. A guest the author wants to follow gets their own person page per [[Logseq/Entity/Person]]. + - ## Frontmatter + - Mark instances with **`logseq-entity:: [[Logseq/Entity/Podcast/Episode]]`** so this definition page collects backlinks to every episode. + - Set **`created-by::`** to the host. + - Set **`date-created::`** to the air date for listening notes, or the recording date for a producer-owned session. + - The publishing garden records feed identity with [[Logseq/Entity/Podcast/Episode/Frontmatter/podcast-guid]] and publication time with [[Logseq/Entity/Podcast/Episode/Frontmatter/podcast-published-at]]. When the episode page there is a [[Logseq/Entity/Proxy/Page]], re-syncing it keeps both. + - Optional: **`logseq-created-time-year::`** linking to the matching [[Logseq/Entity/Time/Year]] instance; see that page for how it differs from `date-created::`. + - **Never add, remove, or edit a `tags::` line on an existing page.** Older episode pages carry assorted `tags::` values; those are the author's and stay as they are. + - ## Page shape + - Logseq Flavored Markdown. + - The first body block is the H1 heading with the episode title linked to the episode when a URL is known: `- # [Episode Title](https://example.com/episode)`. Without a URL, use a plain `- # Episode Title`. + - A producer-owned session opens with an optional short description, artwork when available, and a directly playable MP3 recording. Artwork and audio use Markdown media embeds with the asset URLs: `![Episode artwork](https://example.com/artwork.gif)` and `![Episode recording](https://example.com/recording.mp3)`. + - Downloadable MicroFreak patches use labeled Markdown links to the files, such as `[Download MicroFreak preset A](https://example.com/preset-A.mfpz)`, so listeners can download them from the episode page. Episode media and download details use the direct URLs rather than asset page embeds or asset wikilinks. + - Session notes follow the media and downloads. Technical notes may be collapsed so the recording and patches remain immediately visible. + - In the Gitpa publishing garden, the visible episode properties are `logseq-entity` and `date-created`. + - Recommended body sections are **Notes**, **Highlights**, and **Transcript**, kept only when there is something to put in them. + - Prefer synthesis over pasting a full transcript unless the author asks for the full text. + - ## Combined with YouTube + - An episode published as a YouTube video is also a [[Logseq/Entity/YouTube]] instance, Episode first: `logseq-entity:: [[Logseq/Entity/Podcast/Episode]], [[Logseq/Entity/YouTube]]`. The page keeps the episode path under its show. The YouTube video ID becomes a deduplication key alongside the episode URL. + - **Sources** — a `## Sources` section links the episode page, the YouTube recording, and the [[Snipd]] episode when one exists. + - **Timestamps** — every timestamp is a video time in seconds, written `{{youtube-timestamp }}`. Times from an audio app such as Snipd are converted to video time. The audio and video versions can differ by an intro or an ad break, so check each converted time against the transcript line it should land on. + - **Chapters** — a `## Video` section holds the `{{video }}` block. Each YouTube chapter is an H3 beneath it: `### {{youtube-timestamp }} `. + - **Snips** — a Snipd snip is an H4 under the chapter that contains it, with a `Snipd:` URL line, a short summary, and a `Transcript` child set to `collapsed:: true`. The transcript groups lines under a speaker's person page, each line starting with its own timestamp. + - **Highlights** — a [[Readwise]] highlight made while watching is a bare `{{youtube-timestamp }}` block placed in time order next to the snips. Speaker children hold the quoted text as `>` quotes, ending with a link back to the highlight in Readwise. + - **Expansion** — chapters that hold the author's highlights stay expanded. Chapters without highlights are `collapsed:: true`. + - **What only the video shows** — slides, diagrams, code, and screens that the audio does not describe are captured as frames. A frame is saved in `assets/`, named from the page path with the video second and a short caption, and embedded under the timestamp block where it appears. A child bullet under the image transcribes its text or describes the diagram, so the page reads in full without the video. + - Example: [[Person/Gergely Orosz/Pod/26/09 Design Engineering with Maggie Appleton]]. + - ## Journal + - Record a newly filed episode in the day's journal under `[[Filed]]`, per the journal conventions. The date is the day the page was filed, which is not necessarily the air date. + - ## Examples in this garden + - [[Person/Cal Newport/Pod/25/10/How to Reinvent a Life]] + - [[Person/Gergely Orosz/Podcast/25/10/Beyond Vibe Coding with Addy Osmani]] + - [[Person/Gergely Orosz/Pod/26/09 Design Engineering with Maggie Appleton]] — also a YouTube video diff --git a/gitp-garden/pages/Logseq___Entity___Proxy___Page.md b/gitp-garden/pages/Logseq___Entity___Proxy___Page.md new file mode 100644 index 0000000..748c219 --- /dev/null +++ b/gitp-garden/pages/Logseq___Entity___Proxy___Page.md @@ -0,0 +1,71 @@ +alias:: [[Proxy Page]], [[Logseq Proxy Page]] +entity-tasks:: [[Logseq/Entity/Proxy/Page/mise/Task/sync]] +logseq-entity:: [[Logseq/Entity/Definition]], [[Logseq/Entity/Proxy/Page]] +entity-proxy-destination-properties:: public +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=Logseq%2FEntity%2FProxy%2FPage +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F189-proxy-worktree-identity/pages/Logseq___Entity___Proxy___Page.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # Proxy Page + - In this garden, **Proxy Page** marks a page whose body is mirrored from a page in another [[Logseq/Garden]], so one garden can read a page that lives in another without that page being moved or duplicated by hand. See [[Logseq/Idea/Proxy]] for the motivation. + - ## What counts as an instance + - A page in this garden whose content was copied from a source graph and is kept in step with it by re-syncing. + - A proxy is identified **only** by its `logseq-proxy-url::` frontmatter. Nothing about where the page sits, or what it is named, marks it as a proxy — a page without `logseq-proxy-url::` is a real, unrelated page even when it shares a name with something in a source graph. + - The marker is **additive**. It rides on whatever entity the mirrored page already is, listed after the primary one: `logseq-entity:: [[Logseq/Entity/Book]], [[Logseq/Entity/Proxy/Page]]`. + - Not an instance: a page written here that merely links to another garden; a journal day page, which is out of scope — proxies target `pages/` only. + - ## Naming and links + - A proxy mirrors the source page's **exact** logical name and namespace. Source page `[[My/Page/Here]]` becomes `[[My/Page/Here]]` here — same `___`-encoded filename, no added prefix, no dedicated namespace. This is what lets a whole namespace proxy cleanly: every page keeps its identity across gardens. + - The `page=` value maps to a filename the same way every other page does: `/` becomes `___`, per [[Logseq/Pref/Page/Name]] and the `logseq-get-page-file` command. `page=Logseq/Entity/Podcast` → `pages/Logseq___Entity___Podcast.md`. + - **Legacy shape:** a few early proxies were filed under `Logseq/Proxy//`, for example [[Person/Thomas Parr/Book/2022/Active Inference]]. Leave them where they are; do not file new ones that way. + - ## Frontmatter + - [[Logseq/Entity/Proxy/Page/Frontmatter/logseq-proxy-url]] — required; the source page's [[logseq-url]]. This property is what makes the page a proxy. + - [[Logseq/Entity/Proxy/Page/Frontmatter/logseq-proxy-codeforge-url]] — set when the source graph is pushed to a code forge; the source file's URL there. + - [[Logseq/Entity/Proxy/Page/Frontmatter/logseq-proxy-last-sync-date]] — required; the day the body was last mirrored. + - The proxy keys are owned by the sync process. Do not rename them, and do not treat any other `*::` line on the page as belonging to the proxy. + - Shared frontmatter conventions live on [[Logseq/Frontmatter]]. + - ## Page shape + - No shape of its own. The body is the source page's body, so the source's shape — and the shape of whatever entity the page is primarily — governs. + - ## Invariants + - **`tags::` on an existing page is never touched.** On re-sync the destination's `tags::` win outright and the source's are discarded. On first create, the source's `tags::` come across with the copy unless the user says otherwise. + - **The source owns the properties, except those the destination owns.** A re-sync adds properties the source gained since the last sync, updates those whose value changed, and removes those the source no longer has. The **body** is replaced wholesale by the source's. + - **The destination owns** `tags::`, the proxy keys, and every property an entity definition names in `entity-proxy-destination-properties::`. The definitions consulted are the ones in the source page's `logseq-entity::`, plus this one, which names `public::`: whether a page is published is the destination garden's decision. A re-sync keeps the destination's value of an owned property and never takes the source's, including when the destination has none. + - **A property only the destination has goes on re-sync** unless the destination owns it. A garden that wants to keep a local property declares it on the entity definition. The preview names every property it adds, updates or removes. + - **A name collision is never resolved silently.** If the destination page exists and has no `logseq-proxy-url::`, it is a real page that happens to share the name — stop and ask before writing anything. + - ## Finding and deduplicating + - Proxies scatter across `pages/` wherever their mirrored names fall, so there is no namespace to narrow a search to. Always search repo-wide from the garden root. + - **Every proxy:** `rg -n 'logseq-proxy-url::' pages/ --glob '*.md'` + - **Proxies from one graph:** `rg -n 'logseq://graph/' pages/ --glob '*.md'` + - **Which gardens we proxy from:** `rg -o 'logseq://graph/[^?]+' pages/ --glob '*.md' | sort -u` + - **Which repositories we proxy from:** `rg -o 'logseq-proxy-codeforge-url:: https://[^/]+/[^/]+/[^/]+' pages/ --glob '*.md' | sort -u` + - Before creating one, check the destination path directly — the page name is fully determined by `page=`, so a collision is a file test, not a fuzzy search. + - ## Relationship to other entity types + - **Any entity type** can be proxied; this marker never replaces the primary one, it accompanies it. + - **[[Logseq/Entity/Definition]]** — entity definition pages proxy like anything else, which is how two gardens can share one set of modeling rules. + - ## Locating the source + - **With a code forge URL:** the host, owner and repository in [[Logseq/Entity/Proxy/Page/Frontmatter/logseq-proxy-codeforge-url]] form a `ghq` address, and `ghq list --full-path --exact
` gives the local clone. One path is the clone; no path means the repository is not cloned here, and cloning waits for the human; several paths wait for the human to pick one. The part of the URL between the branch and `pages/` is the graph's folder inside the clone, empty when the graph sits at the repository root. + - **Without one:** the graph name in [[Logseq/Entity/Proxy/Page/Frontmatter/logseq-proxy-url]] resolves through Logseq desktop's own graph list, `~/.logseq/graphs/`, which holds one file per graph with the graph's path in the file name. The graph is the one whose folder carries that name. This resolves only on a machine where Logseq has the graph. + - A graph root holds `pages/` and `logseq/`. + - ## Dependent work across repositories + - `--source` may name a linked source worktree, and `--destination` may name a graph inside a linked destination worktree. Content and companion implementations are read from the selected source checkout. + - The Logseq graph name comes from the primary checkout at the same repository-relative graph path. A graph at the repository root keeps the registered checkout's name; a nested graph keeps its folder name. A linked worktree therefore shares its source graph's logical identity. + - Source ownership combines the repository address, repository-relative graph path, and logical page or implementation path. Branch names and local checkout paths are excluded from ownership, so imports can refresh from a source worktree and later from the merged primary checkout. + - A source worktree's code forge URLs name its branch, or its commit when detached. Push the source branch before opening a dependent destination PR so its source links are followable. After the source PR merges, re-sync from the primary checkout to refresh the links to the default branch. + - Keep the destination's `.logseq-proxy/manifest.json` and imported task files together in its PR. Record the source PR dependency, source and destination branches, and worktree paths in the issue discussion. A relocated clone resolves the same ownership through its repository address and graph path. + - ## Syncing an instance + - **Source file:** the file the code forge URL names, or else `pages/.md` under the graph root, with `page=` decoded (`%2F` is `/`) and each `/` written as `___`. + - **First copy:** the whole source file, with the proxy keys set in its property block. + - **Re-sync:** the source's body under a property block merged per **Invariants** above. Properties already on the destination keep their order; properties new from the source follow them, and the proxy keys come last. + - **Property block:** the run of `key:: value` lines from the top of the file, ending at the first line of any other kind — a blank line, a `- # Heading`, or plain text. A destination with no property block gains one above its first line, holding the proxy keys. + - **Assets:** every `../assets/` the source body links to is copied to the same `assets/` here, replacing what is there. A missing source asset is reported and the sync goes on. + - **Embeds:** with `--follow-embeds`, each page embedded with the `embed` macro is synced as a proxy too, recursively, so a page and the pages it embeds sync in one batch. A missing embedded page is reported and the sync goes on. + - **Report:** the source path, the destination path, the proxy key values written, whether it was a first copy or a re-sync, the properties added, updated or removed, and the assets copied. + - ## Executable sync + - [[Logseq/Entity/Proxy/Page/mise/Task/sync]] previews the page, entity definitions, companion task references, implementation files, and assets imported by a sync. `--apply` applies the previewed changes. + - An instance's `logseq-entity::` links lead to its source entity definitions. Each definition's `entity-tasks::` declares companion task references, whose file mappings and task dependencies follow [[Logseq/Entity/Mise/Task]]. The sync visits shared and cyclic dependencies once. + - `.logseq-proxy/manifest.json` in the destination graph records source repository and graph root, logical page identity, source-to-destination file mappings, ownership, and last imported content hashes. + - The manifest retains the latest source companion declarations. When a proxy becomes the source for another garden, task discovery uses those declarations and resolves imported implementation paths through the source manifest. + - Re-sync refreshes source changes where imported implementation files still match their recorded hashes. Local implementation edits, unowned destination files, and competing source claims produce conflicts before application. + - Page properties and bodies refresh from the source per **Invariants** above. Files removed from upstream task mappings are reported as cleanup candidates and retained locally. + - A preview reports logical entity definitions without files separately from missing declared tasks or implementation files. Missing source assets remain warnings; missing task requirements stop application. + - ## Examples in this garden + - [[Book/ML with PyTorch and Scikit-Learn]] + - [[Person/Thomas Parr/Book/2022/Active Inference]] (legacy name shape) \ No newline at end of file diff --git a/gitp-garden/pages/Logseq___Entity___Proxy___Page___mise___Task___sync.md b/gitp-garden/pages/Logseq___Entity___Proxy___Page___mise___Task___sync.md new file mode 100644 index 0000000..075fc50 --- /dev/null +++ b/gitp-garden/pages/Logseq___Entity___Proxy___Page___mise___Task___sync.md @@ -0,0 +1,64 @@ +logseq-entity:: [[Logseq/Entity/Mise/Task]], [[Logseq/Entity/Proxy/Page]] +task-owner:: [[Person/codekiln/GitHub/logseq-encode-garden]] +task-config-root:: . +task-name:: logseq:entity:proxy:page:sync +task-entrypoint:: mise-tasks/logseq/entity/proxy/page/sync +task-files:: {"mise-tasks/logseq/entity/proxy/page/sync":"mise-tasks/logseq/entity/proxy/page/sync","mise-tasks/logseq/entity/proxy/page/lib/core.py":"mise-tasks/logseq/entity/proxy/page/lib/core.py","mise-tasks/logseq/entity/proxy/page/lib/companions.py":"mise-tasks/logseq/entity/proxy/page/lib/companions.py"} +source-link:: https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/sync +see-also:: [[Logseq/Entity/Proxy/Page]], [[Logseq/Entity/Definition]] +logseq-proxy-url:: logseq://graph/logseq-encode-garden?page=Logseq%2FEntity%2FProxy%2FPage%2Fmise%2FTask%2Fsync +logseq-proxy-codeforge-url:: https://github.com/codekiln/logseq-encode-garden/blob/codex%2F189-proxy-worktree-identity/pages/Logseq___Entity___Proxy___Page___mise___Task___sync.md +logseq-proxy-last-sync-date:: [[2026-10-06]] +- # Sync a Proxy Page and Its Companion Tasks + - Copies a source page into another Logseq garden, together with its entity definitions, declared task references, executable files, helpers, and linked assets. Re-sync refreshes source changes and reports conflicting destination edits before applying changes. + - ## Invocation + - Runs from the source checkout or a destination garden containing the imported file task. Source and destination arguments identify graph roots containing `pages/` and `logseq/`: + - ~~~sh + mise run logseq:entity:proxy:page:sync --source /path/to/logseq-encode-garden --destination /path/to/other-garden --page 'Logseq/Entity/Proxy/Page' + ~~~ + - The command previews the full import. Adding `--apply` writes the changes: + - ~~~sh + mise run logseq:entity:proxy:page:sync --source /path/to/logseq-encode-garden --destination /path/to/other-garden --page 'Logseq/Entity/Proxy/Page' --apply + ~~~ + - `--page` is the source page's logical name, with slash namespaces. On re-sync, `--source` can be omitted when the destination page's proxy metadata resolves to a local source graph. + - `--follow-embeds` also syncs every page the page embeds with the `embed` macro, and the pages those embed, in the same batch. A podcast session and its embedded recording, artwork and preset pages sync together this way: + - ~~~sh + mise run logseq:entity:proxy:page:sync --source /path/to/logseq-encode-garden --destination /path/to/other-garden --page 'GitP/A/Session/26/09/24-Thu' --follow-embeds --apply + ~~~ + - ## Source and destination worktrees + - Select each worktree explicitly while its dependent source changes are under review: + - ~~~sh + python3 /path/to/source-worktree/mise-tasks/logseq/entity/proxy/page/sync --source /path/to/source-worktree --destination /path/to/destination-worktree/garden --page 'Logseq/Entity/Proxy/Page' --apply + ~~~ + - Invoking the source checkout's script imports the current sync implementation along with the entity definition. The destination can then run its imported task for other pages from the same source worktree. + - The source worktree supplies content while the primary checkout supplies the logical graph name. Code forge URLs point to the selected source branch, or to the commit for a detached worktree. The portable manifest keeps the same source ownership across branches and checkouts. + - Commit the manifest with the imported pages and task files. Push the source branch and link the source PR as a dependency of the destination PR. After the source PR merges, re-sync with the primary checkout selected as `--source` to update the source URLs to the default branch. + - Omitting `--source` resolves the registered primary checkout. An unmerged source branch therefore requires an explicit source worktree path on each sync. + - ## Inputs and outputs + - Reads the source page and the entity definitions named by `logseq-entity::`. Definitions declare companion tasks with `entity-tasks::`; task references declare implementation paths and required task dependencies through [[Logseq/Entity/Mise/Task]]. + - Copies pages under their source logical names, adds [[Logseq/Entity/Proxy/Page]] membership, and sets source URL, available code forge URL, and sync date. Source bodies replace destination bodies. + - On re-sync, page properties follow the source: new ones are added, changed ones updated, and ones the source dropped are removed. `tags::`, the proxy keys, and the properties named by an entity definition's `entity-proxy-destination-properties::` keep their destination values. [[Logseq/Entity/Proxy/Page]] states the rule; the preview lists each property added, updated or removed. + - Copies linked `../assets/` files and explicitly mapped task files. Source implementation paths start at the repository root, even when the source graph occupies a subdirectory; destination paths start at the destination graph root. + - Records imported ownership, source paths, destination paths, and last imported hashes in `.logseq-proxy/manifest.json`. A repeat run previews only the changes still needed. + - The manifest keeps current source task declarations and path mappings for onward imports from a proxy garden. + - ## Side effects + - Preview reads local files and reports planned changes, property changes, conflicts, missing assets and embedded pages, and upstream removal candidates. `--apply` creates or refreshes the previewed imports and manifest after validation. + - An existing page without proxy metadata is a name collision. A task file with local edits, an unowned file at the target path, or competing source ownership stops application. Destination edits remain available for review. + - Files removed from upstream task mappings remain in the destination and appear as cleanup candidates. Missing linked source assets are warnings; missing declared task requirements stop application. + - Application stages writes and retains recovery information or restores previous files if a write fails. + - ## Dependencies and access + - Mise supplies Python through the file task's tool metadata. The sync implementation uses Python's standard library and requires local read access to the source and write access to the destination for `--apply`. + - Source discovery follows [[Logseq/Entity/Proxy/Page]]: a code forge URL resolves through locally registered ghq repositories, and a Logseq graph URL can resolve through the desktop graph list. An explicit `--source` selects a graph directly. + - Imported task files retain executable mode. The destination's mise task discovery must include its `mise-tasks/` directory. + - ## Failure and recovery + - Unresolved source: provide `--source` with the graph root or make the existing source repository available locally. + - Page collision: compare the source and destination pages and choose which page belongs in the destination before syncing again. + - Missing task declaration or implementation: repair the source task reference or restore the named file, then preview again. + - Local implementation edit: review the destination file against its source and retain or reconcile the edit before syncing again. + - Interrupted application: run `mise run logseq:entity:proxy:page:sync --destination /path/to/other-garden --recover` to restore the previous files, then run a fresh preview. + - Conflicting ownership: reconcile the source mappings so each destination implementation path has one source owner. + - ## Source and help + - [Sync file task](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/sync), [page sync implementation](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/lib/core.py), and [companion import implementation](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/lib/companions.py). + - ~~~sh + mise run logseq:entity:proxy:page:sync --help + ~~~ diff --git a/mise.toml b/mise.toml index 3b70b2f..6c6c776 100644 --- a/mise.toml +++ b/mise.toml @@ -1,15 +1,25 @@ +[tools] +java = "temurin-21" +babashka = "1.3.191" +"npm:@logseq/nbb-logseq" = { version = "1.2.173", allow_low_downloads = true } + [tasks."rss:build"] -description = "Build the public podcast feed from published episode records" -run = "uv run --no-project --with PyYAML==6.0.2 python scripts/build_rss.py" +description = "Build the public podcast feed from episode pages" +run = "uv run --no-project python scripts/build_rss.py" [tasks."rss:check"] -description = "Check that rss.xml matches published episode records" -run = "uv run --no-project --with PyYAML==6.0.2 python scripts/build_rss.py --check" +description = "Check that rss.xml matches public episode pages" +run = "uv run --no-project python scripts/build_rss.py --check" [tasks."rss:test"] -description = "Test podcast feed generation" -run = "uv run --no-project --with PyYAML==6.0.2 python -m unittest discover -s tests" +description = "Test podcast feed generation and website preparation" +run = "uv run --no-project python -m unittest discover -s tests" + +[tasks."site:query:check"] +description = "Validate the real homepage query against the graph" +run = "nbb-logseq scripts/check_homepage.cljs" -[tasks."episode:import"] -description = "Import a reviewed episode handoff from the knowledge garden" -run = "uv run --no-project --with PyYAML==6.0.2 python scripts/import_episode.py" +[task_config] +# gitp-garden/mise-tasks holds companion tasks imported with garden proxy pages, +# such as logseq:entity:proxy:page:sync. +includes = ["gitp-garden/mise-tasks"] diff --git a/nbb.edn b/nbb.edn new file mode 100644 index 0000000..64b340e --- /dev/null +++ b/nbb.edn @@ -0,0 +1,4 @@ +{:deps {logseq/graph-parser + {:git/url "https://github.com/logseq/logseq" + :git/sha "cd2211a042332792ac3f797e34c08bc77c4e4dd4" + :deps/root "deps/graph-parser"}}} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a9847b4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,589 @@ +{ + "name": "13-render-garden-proxies", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "mldoc": "1.5.9" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "license": "ISC", + "dependencies": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "license": "ISC" + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "license": "MIT", + "dependencies": { + "invert-kv": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "license": "MIT", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "license": "MIT", + "dependencies": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mldoc": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/mldoc/-/mldoc-1.5.9.tgz", + "integrity": "sha512-87FQ7hseS87tsk+VdpIigpu8LH+GwmbbFgpxgFwvnbH5oOjmIrc47laH4Dyggzqiy8/vMjDHkl7vsId0eXhCDQ==", + "license": "ISC", + "dependencies": { + "yargs": "^12.0.2" + }, + "bin": { + "mldoc": "bin/cli.js" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "license": "MIT", + "dependencies": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "license": "MIT", + "dependencies": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "node_modules/yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c173bd8 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "description": "Logseq file parser dependency for homepage query validation", + "dependencies": { "mldoc": "1.5.9" } +} diff --git a/rss.xml b/rss.xml index 175492f..02aeebd 100644 --- a/rss.xml +++ b/rss.xml @@ -21,15 +21,45 @@ Codekiln codekiln@pm.me + + GitP.24.12.04 + https://codekiln.github.io/gitpa/#/page/GitP%2FA%2FSession%2F24%2F12%2F04-Wed + A MicroFreak session recorded on December 4, 2024. + Codekiln + A MicroFreak session recorded on December 4, 2024. + Sun, 04 Oct 2026 14:46:20 -0400 + gitpa20241204 + + + + GitP.24.11.19 + https://codekiln.github.io/gitpa/#/page/GitP%2FA%2FSession%2F24%2F11%2F19-Tue + A MicroFreak session recorded on November 19, 2024. + Codekiln + A MicroFreak session recorded on November 19, 2024. + Sun, 04 Oct 2026 14:46:19 -0400 + gitpa20241119 + + + + GitP.26.09.24 + https://codekiln.github.io/gitpa/#/page/GitP%2FA%2FSession%2F26%2F09%2F24-Thu + A MicroFreak and Launchpad session beginning with two-operator FM on an initialized patch. + Codekiln + A MicroFreak and Launchpad session beginning with two-operator FM on an initialized patch. + Sun, 04 Oct 2026 14:22:23 -0400 + gitpa20260924 + + GitP.26.09.25 - https://codekiln.github.io/gitpa/#/page/Ceremony%2F2026%2F09%2F25 + https://codekiln.github.io/gitpa/#/page/GitP%2FA%2FSession%2F26%2F09%2F25-Fri MicroFreak SAWX and WaveUser preset 1910, with a Novation Launchpad sequence. Codekiln MicroFreak SAWX and WaveUser preset 1910, with a Novation Launchpad sequence. Sat, 03 Oct 2026 12:00:00 -0400 gitpa20260925 - + diff --git a/scripts/build_rss.py b/scripts/build_rss.py index e3240f1..d35fe50 100644 --- a/scripts/build_rss.py +++ b/scripts/build_rss.py @@ -1,4 +1,4 @@ -"""Build the published podcast feed from episode.yml records.""" +"""Build the podcast feed from public Logseq episode proxies.""" from __future__ import annotations @@ -6,10 +6,12 @@ from datetime import datetime from email.utils import format_datetime from pathlib import Path -from urllib.parse import urlparse -import xml.etree.ElementTree as ET +from urllib.parse import urlparse, quote +from urllib.request import Request, urlopen +import re -import yaml +from logseq_page import split_page, properties, page_path +import xml.etree.ElementTree as ET ROOT = Path(__file__).resolve().parents[1] @@ -42,52 +44,56 @@ def https_url(record: dict, name: str, path: Path) -> str: return value -def load_episodes(garden: Path = GARDEN) -> list[dict]: +def audio_metadata(url: str) -> tuple[int, str]: + with urlopen(Request(url, method="HEAD"), timeout=30) as response: + length = int(response.headers.get("Content-Length", "0")) + content_type = response.headers.get("Content-Type", "").split(";", 1)[0] + if length <= 0 or content_type != "audio/mpeg": + raise ValueError(f"{url}: expected a nonempty public audio/mpeg file") + return length, content_type + + +def load_episodes(garden: Path = GARDEN, probe=audio_metadata) -> list[dict]: episodes = [] - guids = set() - audio_urls = set() - for path in sorted((garden / "assets").rglob("episode.yml")): - record = yaml.safe_load(path.read_text(encoding="utf-8")) - if not isinstance(record, dict): - raise ValueError(f"{path}: expected a YAML mapping") - if record.get("published") is not True: + guids, audio_urls = set(), set() + for path in sorted((garden / "pages").glob("*.md")): + lines, body = split_page(path.read_text(encoding="utf-8")) + props = properties(lines) + if props.get("public") != "true": continue - - for name in ("episode_title", "description", "recorded_on", "guid", "page"): - required(record, name, path) - for name in ("audio_url", "page_url"): - https_url(record, name, path) - if urlparse(record["audio_url"]).query or urlparse(record["audio_url"]).fragment: - raise ValueError(f"{path}: audio_url must be a permanent public URL") - if record.get("audio_type") != "audio/mpeg": - raise ValueError(f"{path}: audio_type must be audio/mpeg") - if type(record.get("audio_length")) is not int or record["audio_length"] <= 0: - raise ValueError(f"{path}: audio_length must be a positive integer") - try: - datetime.strptime(record["recorded_on"], "%Y-%m-%d") - published_at = datetime.fromisoformat(required(record, "published_at", path)) - except ValueError as error: - raise ValueError(f"{path}: invalid recording or publication date: {error}") from error - if published_at.tzinfo is None or published_at.utcoffset() is None: - raise ValueError(f"{path}: published_at needs a timezone offset") - - page_name = record["page"] - if page_name.startswith("/") or ".." in page_name.split("/"): - raise ValueError(f"{path}: invalid page name") - page_path = garden / "pages" / (page_name.replace("/", "___") + ".md") - if not page_path.is_file() or "public:: true" not in page_path.read_text(encoding="utf-8").splitlines()[:5]: - raise ValueError(f"{path}: public episode page is missing: {page_path}") - if record["guid"] in guids: - raise ValueError(f"{path}: duplicate GUID {record['guid']}") - if record["audio_url"] in audio_urls: - raise ValueError(f"{path}: duplicate audio URL {record['audio_url']}") - guids.add(record["guid"]) - audio_urls.add(record["audio_url"]) - record["_published_at"] = published_at - episodes.append(record) - - episodes.sort(key=lambda episode: episode["_published_at"], reverse=True) - return episodes + if "[[Logseq/Entity/Podcast/Episode]]" not in props.get("logseq-entity", ""): + continue + guid = required(props, "podcast-guid", path) + published = datetime.fromisoformat(required(props, "podcast-published-at", path)) + if published.tzinfo is None or published.utcoffset() is None: + raise ValueError(f"{path}: podcast-published-at needs a timezone offset") + heading = re.search(r"^- # ([^\n]+)\n\t- ([^\n]+)", body, re.MULTILINE) + if not heading: + raise ValueError(f"{path}: expected an H1 followed by the episode description") + embeds = re.findall(r"\{\{embed\s+\[\[([^\]]+)\]\]\s*\}\}", body) + audio = re.findall(r"!\[[^\]]*\]\((https://[^)]+\.mp3)\)", body) + for name in embeds: + asset_path = page_path(garden, name) + _, asset_body = split_page(asset_path.read_text(encoding="utf-8")) + audio.extend(re.findall(r"!\[[^\]]*\]\((https://[^)]+\.mp3)\)", asset_body)) + if len(audio) != 1: + raise ValueError(f"{path}: expected one MP3 recording link") + url = https_url({"audio_url": audio[0]}, "audio_url", path) + if urlparse(url).query or urlparse(url).fragment: + raise ValueError(f"{path}: audio URL must be permanent") + if guid in guids or url in audio_urls: + raise ValueError(f"{path}: duplicate GUID or audio URL") + guids.add(guid) + audio_urls.add(url) + length, content_type = probe(url) + page_name = path.stem.replace("___", "/") + episodes.append({ + "episode_title": heading[1], "description": heading[2], + "guid": guid, "_published_at": published, + "page_url": SITE_URL + "#/page/" + quote(page_name, safe=""), + "audio_url": url, "audio_length": length, "audio_type": content_type, + }) + return sorted(episodes, key=lambda episode: episode["_published_at"], reverse=True) def render_feed(episodes: list[dict]) -> bytes: @@ -137,7 +143,7 @@ def add(parent: ET.Element, tag: str, value: str) -> None: def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--check", action="store_true", help="Fail if rss.xml differs from episode records") + parser.add_argument("--check", action="store_true", help="Fail if rss.xml differs from episode pages") args = parser.parse_args() episodes = load_episodes() if not episodes: diff --git a/scripts/check_homepage.cljs b/scripts/check_homepage.cljs new file mode 100644 index 0000000..9a42dd1 --- /dev/null +++ b/scripts/check_homepage.cljs @@ -0,0 +1,83 @@ +(ns check-homepage + (:require ["fs" :as fs] + ["os" :as os] + ["path" :as path] + ["child_process" :as child] + [clojure.string :as str] + [clojure.walk :as walk] + [cljs.reader :as reader] + [datascript.core :as d] + [logseq.graph-parser.cli :as parser])) + +(def source (or (first *command-line-args*) "gitp-garden")) +(def temporary (.mkdtempSync fs (path/join (.tmpdir os) "gitpa-query-"))) +(def graph (path/join temporary "prepared")) +(.execFileSync child "python3" #js ["scripts/prepare_site.py" "--source" source "--output" graph]) +(def content (.readFileSync fs (str graph "/pages/Ghost in the Patch.md") "utf8")) +(def query-text (second (re-find #"(?s)#\+BEGIN_QUERY\s*(.*?)\s*#\+END_QUERY" content))) +(def query-map (reader/read-string query-text)) +(def transform (eval (:result-transform query-map))) +(def view (eval (:view query-map))) +(def conn (:conn (parser/parse-graph graph {:verbose false}))) +;; Logseq rewrites (pull ?b [*]) to a fixed block attribute list before running +;; an advanced query, so ?b results lose :block/name and :block/original-name. +;; Source: frontend.db.query-custom/replace-star-with-block-attrs! and +;; frontend.db.model/block-attrs at logseq 0.10.6, the publish-spa default. +(def logseq-block-attrs + '[:db/id :block/uuid :block/parent :block/left :block/collapsed? :block/format + :block/refs :block/_refs :block/path-refs :block/tags :block/content :block/marker + :block/priority :block/properties :block/properties-order :block/properties-text-values + :block/pre-block? :block/scheduled :block/deadline :block/repeated? :block/created-at + :block/updated-at :block/heading-level :block/file + {:block/page [:db/id :block/name :block/original-name :block/journal-day]}]) +(defn logseq-query [query] + (walk/postwalk + (fn [form] + (if (and (seq? form) (= 'pull (first form)) (= '?b (second form)) (= '[*] (nth form 2 nil))) + (list 'pull '?b logseq-block-attrs) + form)) + query)) +(defn result [db] + (transform (map first (d/q (logseq-query (:query query-map)) db)))) +(def expected + (->> (d/q '[:find (pull ?p [*]) :where [?p :block/name]] @conn) + (map first) + (filter #(and (= true (get-in % [:block/properties :public])) + (contains? (get-in % [:block/properties :logseq-entity] #{}) "Logseq/Entity/Podcast/Episode"))) + (sort-by #(js/Date.parse (get-in % [:block/properties :podcast-published-at])) >) + (map :block/name))) +(def actual (map :block/name (result @conn))) +(def rendered-view (view (result @conn))) +(def links (for [li (rest rendered-view)] (second li))) +(assert (= (count expected) (count links)) "Homepage view must link every episode") +(assert (every? #(and (= "a" (first %)) (str/starts-with? (get-in % [1 :href]) "#/page/") + (not (str/blank? (nth % 2)))) links) "Homepage episode link or label missing") +(assert (seq expected) "No public episodes parsed") +(assert (= expected actual) (str "Homepage differs: " actual " expected " expected)) +(def drafts ["false" "true" false]) +(def draft-db + (d/db-with @conn + (map-indexed + (fn [i public] {:db/id (- -1 i) :block/name (str "draft-" i) + :block/properties {:public public :logseq-entity #{"Logseq/Entity/Podcast/Episode"} + :podcast-published-at "2099-01-01"}}) + drafts))) +(assert (= actual (map :block/name (result draft-db))) "Draft or string public value leaked into homepage") +(println "Homepage query returns published episodes newest first:" (str/join ", " actual)) + +;; A real parsed fixture catches timezone ordering independently of the transform. +(def fixture-source (path/join temporary "fixture")) +(.cpSync fs source fixture-source #js {:recursive true}) +(doseq [[name date public] [["timezone-earlier" "2099-10-05T10:00:00+02:00" "true"] + ["timezone-later" "2099-10-05T09:00:00Z" "true"] + ["unpublished" "2100-01-01T00:00:00Z" "false"]]] + (.writeFileSync fs (path/join fixture-source "pages" (str name ".md")) + (str "public:: " public "\nlogseq-entity:: [[Logseq/Entity/Podcast/Episode]]\npodcast-published-at:: " date "\n- # Fixture\n"))) +(def fixture (path/join temporary "fixture-prepared")) +(.execFileSync child "python3" #js ["scripts/prepare_site.py" "--source" fixture-source "--output" fixture]) +(def fixture-conn (:conn (parser/parse-graph fixture {:verbose false}))) +(def fixture-names (map :block/name (result @fixture-conn))) +(assert (= ["timezone-later" "timezone-earlier"] (vec (take 2 fixture-names))) "Timezone ordering is wrong") +(assert (not (some #{"unpublished"} fixture-names)) "Parsed draft leaked into query") +(.rmSync fs temporary #js {:recursive true :force true}) +(println "Timezone and unpublished fixtures pass") diff --git a/scripts/import_episode.py b/scripts/import_episode.py deleted file mode 100644 index 78b3eeb..0000000 --- a/scripts/import_episode.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Import a garden episode handoff into an unpublished front-of-house draft.""" -from __future__ import annotations - -import argparse -from datetime import date -import json -import os -from pathlib import Path -import re -import tempfile -from urllib.parse import urlparse - -import yaml - -ROOT = Path(__file__).resolve().parents[1] -MEDIA = ("audio_url", "audio_length", "audio_type") -REQUIRED = ("recorded_on", "episode_title", "description") - - -def no_duplicate_keys(pairs): - result = {} - for key, value in pairs: - if key in result: - raise ValueError(f"Duplicate handoff field: {key}") - result[key] = value - return result - - -def validate_handoff(data: object) -> tuple[dict, date]: - if not isinstance(data, dict): - raise ValueError("Handoff must be a JSON object") - extra = set(data) - set(REQUIRED + MEDIA) - if extra: - raise ValueError(f"Unsupported handoff fields: {', '.join(sorted(extra))}") - for name in REQUIRED: - if not isinstance(data.get(name), str) or not data[name].strip(): - raise ValueError(f"{name} must be a nonempty string") - if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", data["recorded_on"]): - raise ValueError("recorded_on must use YYYY-MM-DD") - day = date.fromisoformat(data["recorded_on"]) - if "\n" in data["episode_title"] or "\r" in data["episode_title"]: - raise ValueError("episode_title must be a single line") - supplied = [name in data for name in MEDIA] - if any(supplied) and not all(supplied): - raise ValueError("Supply audio_url, audio_length and audio_type together") - if all(supplied): - if not isinstance(data["audio_url"], str): - raise ValueError("audio_url must be a permanent HTTPS URL") - parsed = urlparse(data["audio_url"]) - if (parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password - or parsed.query or parsed.fragment or any(c.isspace() for c in data["audio_url"])): - raise ValueError("audio_url must be a permanent HTTPS URL without credentials") - # Validate malformed ports as well as malformed hosts. - parsed.port - if type(data["audio_length"]) is not int or data["audio_length"] <= 0: - raise ValueError("audio_length must be a positive integer") - if data["audio_type"] != "audio/mpeg": - raise ValueError("audio_type must be audio/mpeg") - return data, day - - -def page_path(garden: Path, name: object) -> Path: - if (not isinstance(name, str) or not name or "\\" in name or name.startswith("/") - or any(part in {"", ".", ".."} for part in name.split("/")) - or any(c in name for c in "\n\r\0") or "___" in name): - raise ValueError("Existing episode has an invalid page name") - path = garden / "pages" / (name.replace("/", "___") + ".md") - # Symlinked page paths must stay in the front garden. - if not path.resolve().is_relative_to((garden / "pages").resolve()): - raise ValueError("Episode page escapes the front garden") - return path - - -def write_batch(changes: dict[Path, bytes], originals: dict[Path, bytes | None]) -> None: - staged = {} - committed = [] - try: - for path, content in changes.items(): - path.parent.mkdir(parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile(dir=path.parent, prefix=".episode-import-", delete=False) as file: - staged[path] = Path(file.name) - file.write(content) - for path, temporary in staged.items(): - current = path.read_bytes() if path.exists() else None - if current != originals[path]: - raise ValueError(f"Episode changed during import: {path}") - os.replace(temporary, path) - committed.append(path) - except Exception: - for path in reversed(committed): - if originals[path] is None: - path.unlink() - else: - path.write_bytes(originals[path]) - raise - finally: - for temporary in staged.values(): - temporary.unlink(missing_ok=True) - - -def import_episode(handoff: Path, root: Path = ROOT) -> tuple[Path, Path]: - data, day = validate_handoff(json.loads(handoff.read_text(encoding="utf-8"), object_pairs_hook=no_duplicate_keys)) - garden = root / "gitp-garden" - record_path = garden / "assets" / "Ceremony" / f"{day:%Y}" / f"{day:%m}" / f"{day:%d}" / "episode.yml" - if not record_path.resolve().is_relative_to((garden / "assets").resolve()): - raise ValueError("Episode record escapes the front garden") - original_record = record_path.read_bytes() if record_path.exists() else None - if original_record is None: - record = {"published": False, "episode_title": data["episode_title"], - "description": data["description"], "recorded_on": day.isoformat(), - "page": f"Ceremony/{day:%Y/%m/%d}"} - record.update({name: data[name] for name in MEDIA if name in data}) - new_record = yaml.safe_dump(record, sort_keys=False, allow_unicode=True).encode() - else: - record = yaml.safe_load(original_record.decode()) - if not isinstance(record, dict): - raise ValueError("Existing episode record must be a YAML mapping") - if str(record.get("recorded_on")) != day.isoformat(): - raise ValueError("Existing episode has a conflicting recorded_on date") - additions = {} - for name in MEDIA: - if name not in data: - continue - if name in record and (type(record[name]) is not type(data[name]) or record[name] != data[name]): - raise ValueError(f"Existing episode has conflicting {name}") - if name not in record: - additions[name] = data[name] - # Preserve the existing text, comments, editorial copy and publication identity. - new_record = original_record - if additions: - new_record = original_record.rstrip(b"\n") + b"\n" + yaml.safe_dump(additions, sort_keys=False).encode() - record.update(additions) - # Check the combined YAML before committing appended enclosure fields. - if yaml.safe_load(new_record.decode()) != record: - raise ValueError("Combined episode record does not match the intended metadata") - page = page_path(garden, record.get("page")) - original_page = page.read_bytes() if page.exists() else None - changes = {} - originals = {record_path: original_record, page: original_page} - if new_record != original_record: - changes[record_path] = new_record - if original_page is None: - if record.get("published") is True: - raise ValueError("Published episode page is missing; restore the reviewed page before importing") - title = record.get("episode_title", data["episode_title"]) - description = record.get("description", data["description"]) - if not isinstance(title, str) or not title.strip() or not isinstance(description, str) or not description.strip(): - raise ValueError("Existing episode needs title and description before creating a page") - # Each prose paragraph stays inside its Logseq block. - title = " ".join(title.splitlines()) - description = " ".join(description.splitlines()) - changes[page] = (f"public:: false\ndate-created:: [[{day:%Y-%m-%d %a}]]\n" - f"type:: [[Podcast/Episode]]\n- # {title}\n\t- {description}\n").encode() - write_batch(changes, originals) - return record_path, page - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("handoff", type=Path, help="Episode handoff JSON from the knowledge garden") - args = parser.parse_args() - try: - record, page = import_episode(args.handoff) - except (ValueError, OSError, yaml.YAMLError) as error: - parser.error(str(error)) - print(f"Episode record: {record}\nDraft page: {page}") - - -if __name__ == "__main__": - main() diff --git a/scripts/logseq_page.py b/scripts/logseq_page.py new file mode 100644 index 0000000..4fda341 --- /dev/null +++ b/scripts/logseq_page.py @@ -0,0 +1,26 @@ +"""Read Logseq page files: the property block, the body, and page-name paths.""" +from __future__ import annotations +import re + + +def split_page(text): + lines = text.splitlines(keepends=True) + count = 0 + for line in lines: + if not re.match(r'^[a-z][a-z0-9-]*:: ', line): + break + count += 1 + return lines[:count], ''.join(lines[count:]) + + +def properties(lines): + return dict(line.rstrip('\n').split(':: ', 1) for line in lines) + + +def page_path(garden, name): + if not name or any(part in ('', '.', '..') for part in name.split('/')) or any(c in name for c in '\\\n\r\x00') or '___' in name: + raise ValueError(f'Invalid page name: {name!r}') + path = garden / 'pages' / (name.replace('/', '___') + '.md') + if not path.resolve().is_relative_to((garden / 'pages').resolve()): + raise ValueError(f'Page escapes garden: {name}') + return path diff --git a/scripts/prepare_site.py b/scripts/prepare_site.py new file mode 100644 index 0000000..8df2cc7 --- /dev/null +++ b/scripts/prepare_site.py @@ -0,0 +1,120 @@ +"""Prepare a website graph with asset visibility derived from public episodes.""" +from __future__ import annotations +import argparse +from datetime import datetime, timezone +from pathlib import Path +import re +import shutil +from urllib.parse import urlparse +from logseq_page import split_page, properties, page_path + +ROOT = Path(__file__).resolve().parents[1] + +def asset_links(body): + return [name for name in re.findall(r'\[\[([^\]]+)\]\]', body) if name.startswith('GitP/A/Session/') and '/Asset/' in name] + +def selected_body(body): + """Select the note sections marked for publication.""" + lines = body.splitlines(keepends=True) + result, skipping, section_indent = [], False, 0 + index = 0 + while index < len(lines): + line = lines[index] + heading = re.match(r'^(\s*)- ## (Notes|Production notes)\s*$', line) + if heading: + section_indent = len(heading[1]) + selected = index + 1 < len(lines) and lines[index + 1].strip() == 'public:: true' + skipping = not selected + if selected: + result.append(line) + index += 2 + continue + elif skipping and line.strip() and len(line) - len(line.lstrip()) <= section_indent: + skipping = False + if not skipping and not re.match(r'^\s*(?:id|collapsed|public)::', line): + result.append(line) + index += 1 + return ''.join(result) + +def listener_body(name, body, pages): + """Resolve the selected recording, artwork, and download embeds.""" + def embed(match): + asset_name = match[1] + if '/Asset/' not in asset_name: + raise ValueError(f'Unsupported episode embed: {asset_name}') + _, asset_body = pages[asset_name] + links = re.findall(r'!?\[([^\]]*)\]\((https://[^)]+)\)', asset_body) + if len(links) != 1: + raise ValueError(f'Expected one public media URL: {asset_name}') + label, url = links[0] + extension = Path(urlparse(url).path).suffix.lower() + if extension in {'.mp3', '.gif', '.png', '.jpg', '.jpeg', '.webp'}: + return f'![{label}]({url})' + parts = asset_name.split('/Asset/', 1)[1].split('/') + label = f'MicroFreak preset {parts[1]}' if parts[0] == 'Preset' else f'{parts[1]} MIDI' if parts[0] == 'MIDI' else label + return f'[{label}]({url})' + return re.sub(r'\{\{embed\s+\[\[([^\]]+)\]\]\s*\}\}', embed, selected_body(body)) + + +def prepare(source, output): + source, output = Path(source).resolve(), Path(output).resolve() + if output.is_relative_to(source) or source.is_relative_to(output): + raise ValueError('Website output must be separate from the source graph') + if output.exists(): + raise ValueError(f'Website output already exists: {output}') + pages = {} + pending = [] + for path in (source / 'pages').glob('*.md'): + lines, body = split_page(path.read_text(encoding='utf-8')) + props = properties(lines) + name = path.stem.replace('___', '/') + pages[name] = (lines, body) + if props.get('public') == 'true' and '[[Logseq/Entity/Podcast/Episode]]' in props.get('logseq-entity', ''): + pending.extend(asset_links(selected_body(body))) + visible = set() + while pending: + name = pending.pop() + if name in visible: + continue + if name not in pages: + raise ValueError(f'Missing linked asset page: {name}') + visible.add(name) + pending.extend(asset_links(pages[name][1])) + rendered = {name: listener_body(name, body, pages) for name, (lines, body) in pages.items() + if properties(lines).get('public') == 'true' and '[[Logseq/Entity/Podcast/Episode]]' in properties(lines).get('logseq-entity', '')} + changes = {} + for name, (lines, body) in pages.items(): + if name in rendered: + keep = {'public', 'logseq-entity', 'podcast-published-at', 'tags'} + lines = [line for line in lines if line.split('::', 1)[0] in keep] + published = properties(lines).get('podcast-published-at') + if published: + date = datetime.fromisoformat(published) + if date.utcoffset() is None: + raise ValueError(f'Missing publication timezone: {name}') + utc = date.astimezone(timezone.utc).isoformat() + lines = [f'podcast-published-at:: {utc}\n' if line.startswith('podcast-published-at::') else line for line in lines] + changes[name] = ''.join(lines) + rendered[name] + continue + if not name.startswith('GitP/A/Session/') or '/Asset/' not in name: + continue + lines = [line for line in lines if not line.startswith('public:: ')] + lines.insert(0, f'public:: {str(name in visible).lower()}\n') + changes[name] = ''.join(lines) + body + shutil.copytree(source, output) + for name, content in changes.items(): + page_path(output, name).write_text(content, encoding='utf-8') + return output + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=ROOT / 'gitp-garden') + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + try: + print(prepare(args.source, args.output)) + except (ValueError, OSError) as error: + parser.exit(1, f'{error}\n') + +if __name__ == '__main__': + main() diff --git a/tests/test_build_rss.py b/tests/test_build_rss.py index bcd3472..0ecba87 100644 --- a/tests/test_build_rss.py +++ b/tests/test_build_rss.py @@ -1,76 +1,78 @@ -from datetime import datetime +import sys from pathlib import Path import tempfile import unittest -import xml.etree.ElementTree as ET +from unittest.mock import Mock, patch +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts')) +from build_rss import load_episodes, render_feed, audio_metadata -from scripts.build_rss import ATOM, ITUNES, load_episodes, render_feed - - -class FeedTest(unittest.TestCase): +class FeedTests(unittest.TestCase): def setUp(self): - self.directory = tempfile.TemporaryDirectory() - self.addCleanup(self.directory.cleanup) - self.garden = Path(self.directory.name) - (self.garden / "assets" / "Ceremony" / "2026" / "09" / "25").mkdir(parents=True) - (self.garden / "pages").mkdir() - (self.garden / "pages" / "Ceremony___2026___09___25.md").write_text("public:: true\n") - self.record = self.garden / "assets" / "Ceremony" / "2026" / "09" / "25" / "episode.yml" - - def write_record(self, extra=""): - self.record.write_text( - "published: true\n" - "episode_title: 'Synth & Signal'\n" - "description: 'Sounds notes'\n" - "recorded_on: '2026-09-25'\n" - "published_at: '2026-10-03T12:00:00-04:00'\n" - "guid: gitpa20260925\n" - "page: Ceremony/2026/09/25\n" - "page_url: https://example.com/episode\n" - "audio_url: https://example.com/audio.mp3\n" - "audio_length: 12345\n" - "audio_type: audio/mpeg\n" + extra - ) - - def test_published_episode_renders_valid_feed(self): - self.write_record() - episodes = load_episodes(self.garden) - self.assertEqual(len(episodes), 1) - self.assertEqual(episodes[0]["_published_at"], datetime.fromisoformat("2026-10-03T12:00:00-04:00")) - rss = ET.fromstring(render_feed(episodes)) - self.assertEqual(rss.findtext("channel/item/title"), "Synth & Signal") - self.assertEqual(rss.findtext("channel/item/description"), "Sounds notes") - self.assertEqual(rss.find("channel/item/guid").attrib["isPermaLink"], "false") - self.assertEqual(rss.find("channel/item/enclosure").attrib["length"], "12345") - self.assertEqual(rss.find("channel/item/enclosure").attrib["type"], "audio/mpeg") - self.assertEqual(rss.find(f"channel/{{{ATOM}}}link").attrib["href"], "https://codekiln.github.io/gitpa/rss.xml") - self.assertIsNotNone(rss.find(f"channel/{{{ITUNES}}}image")) - - def test_legacy_record_remains_unpublished(self): - self.record.write_text("episode_title: Ceremony\nepisode_date: '2024-12-04'\n") - self.assertEqual(load_episodes(self.garden), []) - - def test_missing_public_page_blocks_publication(self): - self.write_record() - (self.garden / "pages" / "Ceremony___2026___09___25.md").unlink() - with self.assertRaisesRegex(ValueError, "public episode page is missing"): - load_episodes(self.garden) - - def test_publication_date_needs_timezone(self): - self.write_record() - original = self.record.read_text() - self.record.write_text(original.replace("2026-10-03T12:00:00-04:00", "2026-10-03T12:00:00")) - with self.assertRaisesRegex(ValueError, "timezone offset"): - load_episodes(self.garden) - - def test_duplicate_audio_url_is_rejected(self): - self.write_record() - second = self.record.parent.parent / "26" / "episode.yml" - second.parent.mkdir() - second.write_text(self.record.read_text().replace("gitpa20260925", "gitpa20260926")) - with self.assertRaisesRegex(ValueError, "duplicate audio URL"): - load_episodes(self.garden) - + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.garden = Path(self.tmp.name) + (self.garden / 'pages').mkdir() + self.page = self.garden / 'pages/GitP___A___Session___26___09___24-Thu.md' + self.asset = self.garden / 'pages/GitP___A___Session___26___09___24-Thu___Asset___Synth___Full___mp3.md' + self.asset.write_text('public:: true\n- # Recording\n\t- ![Listen](https://example.org/audio.mp3)\n') + self.body = '- # A session\n\t- Music from a patch.\n\t- {{embed [[GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3]]}}\n' + self.props = 'public:: true\nlogseq-entity:: [[Logseq/Entity/Podcast/Episode]]\npodcast-guid:: stable\npodcast-published-at:: 2026-10-04T12:00:00-04:00\n' + self.page.write_text(self.props + self.body) + self.probe = Mock(return_value=(1234, 'audio/mpeg')) + def load(self): + return load_episodes(self.garden, self.probe) + def test_body_supplies_copy_and_asset_supplies_audio(self): + episode = self.load()[0] + self.assertEqual(episode['description'], 'Music from a patch.') + self.assertEqual(episode['audio_url'], 'https://example.org/audio.mp3') + self.assertIn(b'length="1234"', render_feed([episode])) + def test_direct_recording_keeps_feed_identity(self): + self.page.write_text(self.props + self.body.replace("{{embed [[GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3]]}}", "![Listen](https://example.org/audio.mp3)")) + self.asset.unlink() + episode = self.load()[0] + self.assertEqual(episode["audio_url"], "https://example.org/audio.mp3") + self.assertEqual(episode["guid"], "stable") + def test_duplicate_recording_link_fails(self): + self.page.write_text(self.props + self.body + "\t- ![Listen](https://example.org/audio.mp3)\n") + with self.assertRaisesRegex(ValueError, "one MP3"): + self.load() + def test_draft_not_probed(self): + self.page.write_text(self.props.replace('public:: true', 'public:: false') + self.body) + self.assertEqual(self.load(), []) + self.probe.assert_not_called() + def test_body_public_marker_cannot_publish(self): + self.page.write_text(self.props.replace('public:: true\n', '') + self.body + '\t- public:: true\n') + self.assertEqual(self.load(), []) + def test_public_episode_requires_feed_identity(self): + self.page.write_text('public:: true\nlogseq-entity:: [[Logseq/Entity/Podcast/Episode]]\n' + self.body) + with self.assertRaisesRegex(ValueError, "podcast-guid"): + self.load() + def test_partial_identity_fails(self): + self.page.write_text(self.props.replace('podcast-guid:: stable\n', '') + self.body) + with self.assertRaisesRegex(ValueError, 'podcast-guid'): + self.load() + def test_timezone_required(self): + self.page.write_text(self.props.replace('T12:00:00-04:00', 'T12:00:00') + self.body) + with self.assertRaisesRegex(ValueError, 'timezone'): + self.load() + def test_duplicate_guid_fails(self): + (self.garden / 'pages/Other.md').write_text(self.props + self.body) + with self.assertRaisesRegex(ValueError, 'duplicate'): + self.load() + def test_broken_asset_fails(self): + self.asset.unlink() + with self.assertRaises(FileNotFoundError): + self.load() + @patch('build_rss.urlopen') + def test_head_metadata_validation(self, opener): + response = Mock() + response.headers = {'Content-Length': '99', 'Content-Type': 'audio/mpeg'} + opener.return_value.__enter__.return_value = response + self.assertEqual(audio_metadata('https://example.org/a.mp3'), (99, 'audio/mpeg')) + self.assertEqual(opener.call_args[0][0].method, 'HEAD') + response.headers['Content-Type'] = 'text/html' + with self.assertRaises(ValueError): + audio_metadata('https://example.org/a.mp3') -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/tests/test_import_episode.py b/tests/test_import_episode.py deleted file mode 100644 index 57f02fa..0000000 --- a/tests/test_import_episode.py +++ /dev/null @@ -1,165 +0,0 @@ -import json -import os -from pathlib import Path -import tempfile -import unittest -from unittest.mock import patch - -import yaml - -from scripts.import_episode import import_episode - - -class ImportEpisodeTests(unittest.TestCase): - def setUp(self): - self.temporary = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) - self.handoff = self.root / 'handoff.json' - self.data = {'recorded_on': '2026-09-24', 'episode_title': 'A synth session', - 'description': 'FM: "bells" and a Launchpad.'} - self.record = self.root / 'gitp-garden/assets/Ceremony/2026/09/24/episode.yml' - self.page = self.root / 'gitp-garden/pages/Ceremony___2026___09___24.md' - - def run_import(self): - self.handoff.write_text(json.dumps(self.data)) - return import_episode(self.handoff, self.root) - - def media(self): - self.data.update(audio_url='https://example.com/episode.mp3', audio_length=1234, - audio_type='audio/mpeg') - - def test_creates_unpublished_record_and_valid_lfm(self): - self.data['description'] += '\n- More listening notes.' - self.run_import() - record = yaml.safe_load(self.record.read_text()) - self.assertFalse(record['published']) - self.assertEqual(record['description'], self.data['description']) - self.assertNotIn('guid', record) - self.assertIn('public:: false', self.page.read_text()) - self.assertNotIn('\n\n', self.page.read_text()) - self.assertNotIn('\n- More listening notes.', self.page.read_text()) - - def test_repeat_preserves_front_copy_page_and_publication_identity(self): - self.run_import() - self.record.write_text("# Human note\npublished: true\nepisode_title: Final title\n" - "description: Final copy\nrecorded_on: '2026-09-24'\n" - "guid: permanent-guid\npublished_at: '2026-10-01T12:00:00-04:00'\n" - "page: Custom/Episode\npage_url: https://example.com/episode\n") - custom = self.root / 'gitp-garden/pages/Custom___Episode.md' - custom.write_text('public:: true\n- My edited episode page\n') - original_record = self.record.read_bytes() - original_page = custom.read_bytes() - self.data['description'] = 'New source description' - self.run_import() - self.assertEqual(self.record.read_bytes(), original_record) - self.assertEqual(custom.read_bytes(), original_page) - - def test_adds_verified_media_without_rewriting_comments_or_copy(self): - self.run_import() - original = '# My comment\n' + self.record.read_text() - self.record.write_text(original) - self.media() - self.run_import() - self.assertTrue(self.record.read_text().startswith(original)) - record = yaml.safe_load(self.record.read_text()) - self.assertEqual(record['audio_length'], 1234) - unchanged = self.record.read_bytes() - self.run_import() - self.assertEqual(self.record.read_bytes(), unchanged) - - def test_conflicting_media_rejected_before_missing_page_is_created(self): - self.media() - self.run_import() - self.page.unlink() - original = self.record.read_bytes() - for name, value in [('audio_url', 'https://example.com/new.mp3'), - ('audio_length', 999), ('audio_type', 'audio/wav')]: - with self.subTest(name=name): - old = self.data[name] - self.data[name] = value - with self.assertRaises(ValueError): - self.run_import() - self.assertEqual(self.record.read_bytes(), original) - self.assertFalse(self.page.exists()) - self.data[name] = old - - def test_validation_failure_creates_no_output(self): - for update in [{'published': True}, {'guid': 'override'}, {'page': '../../escape'}, - {'recorded_on': '2026-02-30'}, {'recorded_on': '2026-9-24'}, - {'episode_title': 'Title\n- Injected block'}, {'description': ''}, - {'audio_url': 'https://example.com/a.mp3'}]: - with self.subTest(update=update): - original = self.data.copy() - self.data.update(update) - with self.assertRaises(ValueError): - self.run_import() - self.assertFalse(self.record.exists()) - self.assertFalse(self.page.exists()) - self.data = original - - def test_invalid_enclosure_creates_no_output(self): - self.media() - for name, value in [('audio_length', True), ('audio_length', 0), ('audio_type', 'text/plain'), - ('audio_url', 'http://example.com/a.mp3'), - ('audio_url', ('https://' + 'fixture-user' + ':' + 'fixture-pass' + '@example.com/a.mp3')), - ('audio_url', 'https://example.com/a.mp3?token=temporary'), - ('audio_url', 'https://example.com:bad/a.mp3')]: - with self.subTest(name=name, value=value): - old = self.data[name] - self.data[name] = value - with self.assertRaises(ValueError): - self.run_import() - self.assertFalse(self.record.exists()) - self.data[name] = old - - def test_conflicting_recording_date_preserves_existing_files(self): - self.run_import() - self.record.write_text(self.record.read_text().replace('2026-09-24', '2026-09-25')) - before = self.record.read_bytes() - with self.assertRaises(ValueError): - self.run_import() - self.assertEqual(self.record.read_bytes(), before) - - def test_invalid_existing_page_does_not_append_media(self): - self.run_import() - self.record.write_text(self.record.read_text().replace('Ceremony/2026/09/24', '../escape')) - before = self.record.read_bytes() - self.media() - with self.assertRaises(ValueError): - self.run_import() - self.assertEqual(self.record.read_bytes(), before) - - def test_published_missing_page_does_not_gain_a_private_draft(self): - self.run_import() - self.record.write_text(self.record.read_text().replace('published: false', 'published: true')) - self.page.unlink() - self.media() - before = self.record.read_bytes() - with self.assertRaises(ValueError): - self.run_import() - self.assertEqual(self.record.read_bytes(), before) - self.assertFalse(self.page.exists()) - - def test_duplicate_json_key_rejected(self): - self.handoff.write_text('{"recorded_on":"2026-09-24","recorded_on":"2026-09-25"}') - with self.assertRaises(ValueError): - import_episode(self.handoff, self.root) - self.assertFalse(self.record.exists()) - - def test_second_write_failure_rolls_back_first(self): - original_replace = os.replace - def fail_page(source, target): - if target == self.page: - raise OSError('Simulated page write failure') - return original_replace(source, target) - with patch('scripts.import_episode.os.replace', side_effect=fail_page): - with self.assertRaises(OSError): - self.run_import() - self.assertFalse(self.record.exists()) - self.assertFalse(self.page.exists()) - self.assertEqual(list(self.root.rglob('.episode-import-*')), []) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_prepare_site.py b/tests/test_prepare_site.py new file mode 100644 index 0000000..82ef9b5 --- /dev/null +++ b/tests/test_prepare_site.py @@ -0,0 +1,107 @@ +import sys +from pathlib import Path +import tempfile +import unittest +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts')) +from prepare_site import prepare, listener_body + +class VisibilityTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.source = Path(self.tmp.name) / 'source' + (self.source / 'pages').mkdir(parents=True) + self.output = Path(self.tmp.name) / 'site' + self.name = 'GitP/A/Session/26/09/24-Thu/Asset/Synth/Full/mp3' + self.asset_file = self.name.replace('/', '___') + '.md' + (self.source / 'pages' / self.asset_file).write_text('public:: false\n- # Audio\n\t- ![Recording](https://example.com/audio.mp3)\n') + def session(self, name, public): + (self.source / 'pages' / f'{name}.md').write_text(f'public:: {str(public).lower()}\nlogseq-entity:: [[Logseq/Entity/Podcast/Episode]]\n- # Session\n\t- {{{{embed [[{self.name}]]}}}}\n') + def asset(self): + return (self.output / 'pages' / self.asset_file).read_text() + def test_public_episode_exposes_asset_in_generated_graph_only(self): + self.session('Public', True) + prepare(self.source, self.output) + self.assertIn('public:: true', self.asset()) + self.assertIn('public:: false', (self.source / 'pages' / self.asset_file).read_text()) + def test_draft_hides_previously_public_asset(self): + self.session('Draft', False) + (self.source / 'pages' / self.asset_file).write_text('public:: true\n- # Audio\n') + prepare(self.source, self.output) + self.assertIn('public:: false', self.asset()) + def test_shared_asset_visible_if_any_parent_public(self): + self.session('Draft', False) + self.session('Public', True) + prepare(self.source, self.output) + self.assertIn('public:: true', self.asset()) + def test_download_links_expose_assets(self): + self.session('Public', True) + session = self.source / 'pages/Public.md' + session.write_text(session.read_text().replace('{{embed ', '').replace('}}', '')) + prepare(self.source, self.output) + self.assertIn('public:: true', self.asset()) + def test_missing_asset_aborts_before_copy(self): + self.session('Public', True) + (self.source / 'pages' / self.asset_file).unlink() + with self.assertRaisesRegex(ValueError, 'Missing linked asset'): + prepare(self.source, self.output) + self.assertFalse(self.output.exists()) + def test_output_inside_source_rejected(self): + with self.assertRaisesRegex(ValueError, 'separate'): + prepare(self.source, self.source / 'site') + +class PresentationTests(unittest.TestCase): + def test_media_and_downloads_use_direct_links(self): + root = 'GitP/A/Session/24/11/19-Tue/Asset/' + names = [root + 'Synth/Full/mp3', root + 'Artwork/Overview/gif', root + 'Preset/A/mfpz', root + 'MIDI/Microfreak/mid'] + pages = {name: ([], f'- ![Media](https://example.com/file.{name.split("/")[-1]})\n') for name in names} + body = ''.join(f'- {{{{embed [[{name}]]}}}}\n' for name in names) + rendered = listener_body('Episode', body, pages) + self.assertIn('![Media](https://example.com/file.mp3)', rendered) + self.assertIn('![Media](https://example.com/file.gif)', rendered) + self.assertIn('[MicroFreak preset A](https://example.com/file.mfpz)', rendered) + self.assertIn('[Microfreak MIDI](https://example.com/file.mid)', rendered) + self.assertNotIn('{{embed', rendered) + def test_only_selected_notes_survive(self): + body = '- # Episode\n\t- ## Notes\n\t\t- Private idea\n\t- ## Files\n\t\t- Download\n' + self.assertNotIn('Private idea', listener_body('Episode', body, {})) + self.assertIn('Download', listener_body('Episode', body, {})) + body = body.replace('## Notes\n', '## Notes\n\t public:: true\n') + rendered = listener_body('Episode', body, {}) + self.assertIn('Private idea', rendered) + self.assertNotIn('public::', rendered) + def test_excluded_notes_do_not_resolve_embeds(self): + body = '- # Episode\n\t- ## Notes\n\t\t- {{embed [[Missing/Page]]}}\n' + self.assertEqual('- # Episode\n', listener_body('Episode', body, {})) + def test_hidden_notes_do_not_publish_unused_assets(self): + with tempfile.TemporaryDirectory() as directory: + source, output = Path(directory) / 'source', Path(directory) / 'output' + (source / 'pages').mkdir(parents=True) + name = 'GitP/A/Session/Test/Asset/Synth/Full/mp3' + asset = source / 'pages' / (name.replace('/', '___') + '.md') + asset.write_text('- ![Recording](https://example.com/test.mp3)\n') + (source / 'pages/Episode.md').write_text('public:: true\nlogseq-entity:: [[Logseq/Entity/Podcast/Episode]]\n- # Episode\n\t- ## Notes\n\t\t- {{embed [[' + name + ']]}}\n') + prepare(source, output) + self.assertTrue((output / 'pages' / asset.name).read_text().startswith('public:: false\n')) + def test_invalid_publication_aborts_before_copy(self): + with tempfile.TemporaryDirectory() as directory: + source, output = Path(directory) / 'source', Path(directory) / 'output' + (source / 'pages').mkdir(parents=True) + (source / 'pages/Episode.md').write_text('public:: true\nlogseq-entity:: [[Logseq/Entity/Podcast/Episode]]\npodcast-published-at:: 2026-01-01\n- # Episode\n') + with self.assertRaisesRegex(ValueError, 'timezone'): + prepare(source, output) + self.assertFalse(output.exists()) + def test_proxy_properties_removed_from_generated_episode(self): + with tempfile.TemporaryDirectory() as directory: + source, output = Path(directory) / 'source', Path(directory) / 'output' + (source / 'pages').mkdir(parents=True) + path = source / 'pages/Episode.md' + original = 'tags:: [[Music]]\npublic:: true\nlogseq-entity:: [[Logseq/Entity/Podcast/Episode]]\npodcast-published-at:: 2026-01-01T00:00:00Z\nlogseq-proxy-url:: logseq://graph/source\n- # Episode\n' + path.write_text(original) + prepare(source, output) + self.assertNotIn('logseq-proxy-url', (output / 'pages/Episode.md').read_text()) + self.assertTrue((output / 'pages/Episode.md').read_text().startswith('tags:: [[Music]]\n')) + self.assertEqual(original, path.read_text()) + +if __name__ == '__main__': + unittest.main()