diff --git a/.cursor/rules/cli-development.mdc b/.cursor/rules/cli-development.mdc new file mode 100644 index 0000000..b2b3b76 --- /dev/null +++ b/.cursor/rules/cli-development.mdc @@ -0,0 +1,58 @@ +# CLI Development Guide + +## CLI Structure +- Main CLI entry point: [src/guhcampos/cli.py](mdc:src/guhcampos/cli.py) +- Uses Click framework for command-line interface +- Entry point defined in [pyproject.toml](mdc:pyproject.toml) as `guhcampos` + +## Command Organization +- Main group: `guhcampos` with subcommands +- Keep CLI logic minimal, delegate to business logic + +## CLI Patterns +- Use Click groups for command organization +- Import pre-baked functions, not direct class instantiation +- Use Rich for console output and progress bars +- Handle errors gracefully with user-friendly messages + +## Command Structure +```python +@guhcampos.command() +def build(): + """Build the Hugo site""" + # Minimal CLI logic + # Call business logic functions +``` + +## Error Handling +- Catch exceptions and provide helpful error messages +- Use Rich console for colored output +- Validate inputs before processing +- Provide usage examples in docstrings + +## Configuration +- Use environment variables for sensitive data +- Load configuration from [src/guhcampos/settings.py](mdc:src/guhcampos/settings.py) +- Support both required and optional parameters +- Use Click options for configuration + +## User Experience +- Provide progress feedback for long-running operations +- Use descriptive command and option names +- Include help text for all commands +- Support both interactive and non-interactive modes + +## Testing CLI +- Test CLI commands with mocked dependencies +- Verify correct function calls and parameters +- Test error handling and user feedback +- Use Click's testing utilities when needed + +## Key Commands +- `guhcampos build` - Build Hugo site +- `guhcampos spotify list-playlists` - List Spotify playlists +- `guhcampos spotify fetch-playlists` - Fetch playlists into M3U and JSON files +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/hugo-development.mdc b/.cursor/rules/hugo-development.mdc new file mode 100644 index 0000000..813b01f --- /dev/null +++ b/.cursor/rules/hugo-development.mdc @@ -0,0 +1,42 @@ +# Hugo Development Guide + +## Hugo Site Structure +- All Hugo content is in [hugo/](mdc:hugo/) directory +- Configuration in [hugo/hugo.toml](mdc:hugo/hugo.toml) +- Theme is managed as git submodule in [hugo/themes/](mdc:hugo/themes/) +- Layouts in [hugo/layouts/](mdc:hugo/layouts/) +- Content in [hugo/content/](mdc:hugo/content/) (if exists) + +## Build Process +- Use Python CLI: `uv run guhcampos build` to build Hugo site +- Use Python CLI: `uv run guhcampos serve` to serve locally +- Build output goes to [hugo/public/](mdc:hugo/public/) +- Generated content (Spotify playlists) goes to [build/](mdc:build/) + +## Configuration +- Hugo configuration in [hugo/config/_default/](mdc:hugo/config/_default/) +- Environment-specific configs in [hugo/config/](mdc:hugo/config/) +- Theme configuration in [hugo/themes/blowfish/](mdc:hugo/themes/blowfish/) + +## Content Integration +- Spotify playlists are fetched and saved as M3U files +- Content can be generated from external APIs +- Use Hugo shortcodes for dynamic content +- Static assets in [hugo/static/](mdc:hugo/static/) (if exists) + +## Development Workflow +1. Edit Hugo content in [hugo/](mdc:hugo/) directory +2. Use Python tools to fetch external content +3. Build with `uv run guhcampos build` +4. Test locally with `uv run guhcampos serve` +5. Deploy via GitHub Actions + +## Key Files +- [hugo/hugo.toml](mdc:hugo/hugo.toml) - Main Hugo configuration +- [hugo/layouts/](mdc:hugo/layouts/) - Custom layouts and templates +- [hugo/archetypes/](mdc:hugo/archetypes/) - Content templates +- [.gitmodules](mdc:.gitmodules) - Theme submodule configuration +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc new file mode 100644 index 0000000..a7560b2 --- /dev/null +++ b/.cursor/rules/project-structure.mdc @@ -0,0 +1,35 @@ +# Project Structure Guide + +This is a personal website project using Hugo with Python build tools. The project has a hybrid structure: + +## Core Structure +- [hugo/](mdc:hugo/) - Contains all Hugo-related content (the actual website) +- [src/guhcampos/](mdc:src/guhcampos/) - Python package with build tools and CLI +- [tests/](mdc:tests/) - Python tests for the build tools +- [build/](mdc:build/) - Generated build artifacts (gitignored) + +## Key Configuration Files +- [pyproject.toml](mdc:pyproject.toml) - Python project configuration and dependencies +- [hugo/hugo.toml](mdc:hugo/hugo.toml) - Hugo site configuration +- [.envrc](mdc:.envrc) - Environment variables for development +- [.gitmodules](mdc:.gitmodules) - Git submodules (Hugo themes) + +## Python Package Structure +- [src/guhcampos/cli.py](mdc:src/guhcampos/cli.py) - Click-based CLI entry point +- [src/guhcampos/hugo.py](mdc:src/guhcampos/hugo.py) - Hugo-specific operations +- [src/guhcampos/spotify/](mdc:src/guhcampos/spotify/) - Spotify API integration +- [src/guhcampos/settings.py](mdc:src/guhcampos/settings.py) - Configuration management + +## Development Tools +- Uses `uv` for Python dependency management +- Ruff for linting and formatting +- Pytest for testing +- VS Code configuration in [.vscode/](mdc:.vscode/) + +## Build Process +The Python package provides CLI commands to build and serve the Hugo site, with +additional integrations for Spotify playlists and other content sources. +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/python-development.mdc b/.cursor/rules/python-development.mdc new file mode 100644 index 0000000..b32241e --- /dev/null +++ b/.cursor/rules/python-development.mdc @@ -0,0 +1,43 @@ +# Python Development Standards + +## Code Style +- Use Black for formatting (88 character line length) +- Use Ruff for linting and import sorting +- Follow type hints with Pydantic models +- Use `uv` for dependency management + +## Project Structure +- All Python code goes in [src/guhcampos/](mdc:src/guhcampos/) +- Tests go in [tests/](mdc:tests/) +- Use Pydantic models for data validation +- Keep CLI logic separate from business logic + +## Testing +- Use pytest with `tmp_path` fixture for file operations +- Mock external APIs (Spotify, etc.) +- Test both success and failure scenarios +- Use descriptive test names and docstrings + +## Dependencies +- Core dependencies in [pyproject.toml](mdc:pyproject.toml) +- Development dependencies include pytest, ruff, black +- Use Pydantic for data models and validation +- Use Click for CLI interface +- Use Rich for console output + +## Key Patterns +- Use generators for pagination (Spotify API) +- Use Pydantic models for API responses +- Use pathlib.Path for file operations +- Use environment variables for configuration +- Use logging/console output for user feedback + +## File Organization +- [src/guhcampos/cli.py](mdc:src/guhcampos/cli.py) - CLI entry points only +- [src/guhcampos/core.py](mdc:src/guhcampos/core.py) - Core utilities +- [src/guhcampos/spotify/](mdc:src/guhcampos/spotify/) - Spotify integration +- [src/guhcampos/settings.py](mdc:src/guhcampos/settings.py) - Configuration +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/spotify-integration.mdc b/.cursor/rules/spotify-integration.mdc new file mode 100644 index 0000000..ee7fe5c --- /dev/null +++ b/.cursor/rules/spotify-integration.mdc @@ -0,0 +1,46 @@ +# Spotify Integration Guide + +## Architecture +- Spotify integration in [src/guhcampos/spotify/](mdc:src/guhcampos/spotify/) +- [src/guhcampos/spotify/client.py](mdc:src/guhcampos/spotify/client.py) - API client and business logic +- [src/guhcampos/spotify/models.py](mdc:src/guhcampos/spotify/models.py) - Pydantic models for data validation + +## Key Patterns +- Use Pydantic models for API responses (SpotifyPlaylist, SpotifyTrack) +- Implement pagination using generators in `fetch_playlists()` +- Use environment variables for API credentials +- Save playlists as M3U files for Hugo integration + +## API Usage +- Client credentials flow for authentication +- Pagination with offset/limit for playlists +- Track sorting by artist and name +- Error handling with console output + +## Data Models +- `SpotifyPlaylist`: Playlist metadata with tracks +- `SpotifyTrack`: Track information with duration +- `to_m3u()` method for playlist export +- `to_json()` method for data serialization + + +## Testing +- Mock API responses in [tests/test_spotify.py](mdc:tests/test_spotify.py) +- Use `tmp_path` fixture for file operations +- Test pagination with realistic mock data +- Test both success and error scenarios + +## Configuration +- Spotify credentials in environment variables +- Output directory configurable via CLI +- Playlist filtering by name +- Progress reporting with Rich console + +## File Formats +- M3U format for playlist export +- JSON format for data storage +- Proper URL validation with Pydantic HttpUrl +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/testing-standards.mdc b/.cursor/rules/testing-standards.mdc new file mode 100644 index 0000000..25a0761 --- /dev/null +++ b/.cursor/rules/testing-standards.mdc @@ -0,0 +1,53 @@ +# Testing Standards + +## Test Structure +- All tests in [tests/](mdc:tests/) directory +- Use pytest as testing framework +- Follow naming convention: `test_*.py` files +- Use descriptive test function names + +## Mocking Patterns +- Mock external APIs (Spotify, HTTP requests) +- Use `@patch` decorator for dependency injection +- Mock file system operations with `tmp_path` fixture +- Create realistic mock data for pagination testing + +## File Operations +- Always use `tmp_path` fixture for file output +- Never write to repository root in tests +- Clean up temporary files automatically +- Test both file creation and content validation + +## API Testing +- Mock HTTP responses with realistic data +- Test pagination logic with multiple mock responses +- Test error scenarios and exception handling +- Validate response parsing and model creation + +## Test Data +- Use Pydantic models for test data validation +- Create realistic mock responses matching API format +- Test edge cases (empty responses, pagination boundaries) +- Use valid URLs for HttpUrl validation + +## Assertions +- Test both success and failure paths +- Validate data types and model instances +- Check function call counts and parameters +- Verify file content and format + +## Test Organization +- Group related tests in same file +- Use descriptive docstrings for test functions +- Test one concept per test function +- Use setup/teardown when needed + +## Key Examples +- [tests/test_spotify.py](mdc:tests/test_spotify.py) - Spotify API testing +- Pagination testing with realistic mock data +- File output testing with `tmp_path` +- Model validation testing +description: +globs: +alwaysApply: false +--- diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml index c3ab8c2..5c7f42f 100644 --- a/.github/workflows/hugo.yml +++ b/.github/workflows/hugo.yml @@ -1,24 +1,16 @@ -name: Deploy Hugo site to Pages +name: build guhcampos.github.io on: push: - branches: ["main"] - workflow_dispatch: - schedule: - - cron: '0 */6 * * *' # Run every 6 hours to sync new content -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read - pages: write id-token: write -# Allow only one concurrent deployment concurrency: group: "pages" cancel-in-progress: false -# Default to bash defaults: run: shell: bash @@ -27,69 +19,68 @@ jobs: build: runs-on: ubuntu-latest env: - HUGO_VERSION: 0.147.7 - OBSIDIAN_VAULT: ${{ github.workspace }}/obsidian-vault - HUGO_CONTENT: ${{ github.workspace }}/content + GUHCAMPOS_OBSIDIAN_VAULT_ROOT: ${{ github.workspace }}/obsidian-vault + GUHCAMPOS_SPOTIFY_CLIENT_ID: ${{ secrets.GUHCAMPOS_SPOTIFY_CLIENT_ID }} + GUHCAMPOS_SPOTIFY_CLIENT_SECRET: ${{ secrets.GUHCAMPOS_SPOTIFY_CLIENT_SECRET }} + GUHCAMPOS_SPOTIFY_USER_ID: ${{ secrets.GUHCAMPOS_SPOTIFY_USER_ID }} + HUGO_VERSION: 0.148.1 steps: - - name: Checkout website repository + - name: checkout website repository uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - - name: Checkout Obsidian vault + - name: checkout obsidian vault uses: actions/checkout@v4 with: - repository: guhcampos/guhcampos-obsidian - token: ${{ secrets.OBSIDIAN_PAT }} - path: obsidian-vault + repository: ${{ secrets.OBSIDIAN_VAULT }} + ssh-key: ${{ secrets.OBSIDIAN_DEPLOY_KEY }} + path: build/obsidian-vault + fetch-depth: 1 - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - cache: 'pip' + - name: install uv + uses: astral-sh/setup-uv@v6 - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install pyyaml + - name: install python + run: uv python install - - name: Setup Hugo - uses: peaceiris/actions-hugo@v2 + - name: install hugo + uses: peaceiris/actions-hugo@v3 with: - hugo-version: '0.147.7' + hugo-version: ${{ env.HUGO_VERSION }} extended: true - - name: Sync Obsidian content - run: python scripts/sync-obsidian + - name: install python dependencies + run: uv sync --locked --all-extras --dev + + - name: copy obsidian content + run: uv run guhcampos obsidian pull + - name: fetch spotify playlists + run: uv run guhcampos spotify fetch-playlists + - name: build website + run: uv run guhcampos hugo build - - name: Setup Pages + - name: setup Pages id: pages uses: actions/configure-pages@v4 - - name: Build with Hugo - env: - HUGO_ENVIRONMENT: production - HUGO_ENV: production - run: | - hugo \ - --gc \ - --minify \ - --baseURL "${{ steps.pages.outputs.base_url }}/" - - - name: Upload artifact + - name: upload artifacts uses: actions/upload-pages-artifact@v3 with: path: ./public deploy: + needs: build + if: github.ref == 'refs/heads/main' + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: build steps: - - name: Deploy to GitHub Pages + - name: deploy to github pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index b3fe63f..62906cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,109 @@ -# Generated files by hugo -/public/ -/resources/_gen/ -/assets/jsconfig.json -hugo_stats.json - -# Executable may be added to repository -hugo.exe -hugo.darwin -hugo.linux - -# Temporary lock file while building -/.hugo_build.lock - -# OS generated files +__marimo__/ +__pycache__/ +__pypackages__/ +._* +.abstra/ +.cache +.coverage +.coverage.* +.dmypy.json .DS_Store .DS_Store? -._* +.eggs/ +.env +.envrc +.envrc +.hypothesis/ +.installed.cfg +.ipynb_checkpoints +.mypy_cache/ +.nox/ +.pdm-build/ +.pdm-python +.pixi +.pybuilder/ +.pypirc +.pyre/ +.pytest_cache/ +.Python +.pytype/ +.ropeproject +.ruff_cache/ +.scrapy .Spotlight-V100 +.spyderproject +.spyproject +.streamlit/secrets.toml +.tox/ .Trashes -ehthumbs.db -Thumbs.db - -# Content directory - generated from Obsidian +.venv +.vscode/ +.webassets-cache +*.cover +*.egg +*.egg-info/ +*.log +*.manifest +*.mo +*.pot +*.py.cover +*.py[codz] +*.sage.py +*.so +*.spec +**/__pycache__/ +*$py.class +/.hugo_build.lock +/assets/jsconfig.json /content/ - -# Log files /logs/ - -.envrc +/public/ +/resources/_gen/ +/site +build/ +celerybeat-schedule +celerybeat.pid +cover/ +coverage.xml +cython_debug/ +db.sqlite3 +db.sqlite3-journal +develop-eggs/ +dist/ +dmypy.json +docs/_build/ +downloads/ +eggs/ +ehthumbs.db +env.bak/ +env/ +ENV/ +htmlcov/ +hugo_stats.json +hugo.darwin +hugo.exe +hugo.linux +hugo/data/playlists/ +instance/ +ipython_config.py +lib/ +lib64/ +local_settings.py +MANIFEST +marimo/_lsp/ +marimo/_static/ +nosetests.xml +parts/ +pip-delete-this-directory.txt +pip-log.txt +profile_default/ +public/ +sdist/ +share/python-wheels/ +static/ +target/ +Thumbs.db +var/ +venv.bak/ +venv/ +wheels/ diff --git a/.gitmodules b/.gitmodules index 426d936..ba5bf93 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ -[submodule "themes/blowfish"] - path = themes/blowfish +[submodule "hugo/themes/blowfish"] + path = hugo/themes/blowfish url = https://github.com/nunocoracao/blowfish.git branch = main diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d21b68..31e2405 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,28 @@ repos: -- repo: https://github.com/Yelp/detect-secrets - rev: v1.5.0 - hooks: - - id: detect-secrets - args: ['--baseline', '.secrets.baseline'] - exclude: package.lock.json|poetry.lock|yarn.lock + - repo: https://github.com/gitleaks/gitleaks + rev: v8.24.2 + hooks: + - id: gitleaks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-json + - id: check-shebang-scripts-are-executable + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: debug-statements + - id: detect-private-key + - id: pretty-format-json + args: ['--indent', '2', '--autofix'] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.3 + hooks: + - id: ruff-check + - id: ruff-format diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e517cc --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# guhcampos.github.io + +This is the sorce repository of my personal webpage, hosted on +https://guhcampos.github.io. + +As you see I decided to open source the whole +thing just to make the whole process public as I plan to add interesting stuff +here, such as fetching content remotely from multiple platforms I use on my +personal life. In the end, it doubles as an experimental platform for me to play +with a bunch of adjacent technologies, from some frontend stuff I'm not too +familiar with to the new AI tooling around. + +It should be secure as a public repository. If it isn't, then I'm the one to +blame anyway so there's that. diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml deleted file mode 100644 index d89e764..0000000 --- a/config/_default/hugo.toml +++ /dev/null @@ -1,72 +0,0 @@ -theme = "blowfish" -baseURL = "https://guhcampos.github.io/" -defaultContentLanguage = "en" -pluralizeListTitles = "true" -enableRobotsTXT = true -summaryLength = 0 - -buildDrafts = false -buildFuture = false - -enableEmoji = true - -# googleAnalytics = "G-XXXXXXXXX" - -[pagination] - pagerSize = 100 - -[imaging] - anchor = 'Center' - -[taxonomies] - tag = "tags" - category = "categories" - author = "authors" - series = "series" - -[sitemap] - changefreq = 'daily' - filename = 'sitemap.xml' - priority = 0.5 - -[outputs] - home = ["HTML", "RSS", "JSON"] - -[related] - threshold = 0 - toLower = false - - [[related.indices]] - name = "tags" - weight = 100 - - [[related.indices]] - name = "categories" - weight = 100 - - [[related.indices]] - name = "series" - weight = 50 - - [[related.indices]] - name = "authors" - weight = 20 - - [[related.indices]] - name = "date" - weight = 10 - - [[related.indices]] - applyFilter = false - name = 'fragmentrefs' - type = 'fragments' - weight = 10 - -[markup] - [markup.goldmark] - [markup.goldmark.extensions] - [markup.goldmark.extensions.passthrough] - enable = true - [markup.goldmark.extensions.passthrough.delimiters] - block = [['\[', '\]'], ['$$', '$$']] - inline = [['\(', '\)']] diff --git a/config/_default/languages.en.toml b/config/_default/languages.en.toml deleted file mode 100644 index cb292c8..0000000 --- a/config/_default/languages.en.toml +++ /dev/null @@ -1,46 +0,0 @@ -disabled = false -languageCode = "en" -languageName = "English" -weight = 1 -title = "guhcampos" - -[params] -displayName = "EN" -isoCode = "en" -rtl = false -dateFormat = "2 January 2006" -description = "Guhcampos Digital Garden" - - [params.author] - name = "Gustavo Campos" - email = "guhcampos@gmail.com" - headline = "Brazilian, Millenial, Tinkerer, SCUBA diver, Nerd." - bio = "Brazilian, Millenial, Tinkerer, Nerd. I write about random stuff and for some reason I decided to start publishing it." - - [[params.author.links]] - github = "https://github.com/guhcampos" - - [[params.author.links]] - gitlab = "https://gitlab.com/guhcampos" - - [[params.author.links]] - instagram = "https://www.instagram.com/guhcampos.stuff/" - - [[params.author.links]] - lastfm = "https://www.last.fm/user/guhcampos" - - [[params.author.links]] - linkedin = "https://www.linkedin.com/in/gucampos/" - - [[params.author.links]] - reddit = "https://www.reddit.com/user/guhcampos/" - - [[params.author.links]] - spotify = "https://open.spotify.com/user/12132336829" - - [[params.author.links]] - steam = "https://steamcommunity.com/id/guhcampos/" - -["params.author"] -headline = "Brazilian, Millenial, Tinkerer, SCUBA diver, Nerd." -bio = "I'm Gustavo Campos, a Software, Platform and Site Reliability Engineer based in Belo Horizonte, Brazil. This website is populated with random stuff I write and somehow still think I should publish even after proof-reading it." diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml deleted file mode 100644 index 062bc0e..0000000 --- a/config/_default/menus.en.toml +++ /dev/null @@ -1,69 +0,0 @@ -# -- Main Menu -- -# The main menu is displayed in the header at the top of the page. -# Acceptable parameters are name, pageRef, page, url, title, weight. -# -# The simplest menu configuration is to provide: -# name = The name to be displayed for this menu link -# pageRef = The identifier of the page or section to link to -# -# By default the menu is ordered alphabetically. This can be -# overridden by providing a weight value. The menu will then be -# ordered by weight from lowest to highest. - -#[[main]] -# name = "Blog" -# pageRef = "posts" -# weight = 10 - -#[[main]] -# name = "Parent" -# weight = 20 - -#[[main]] -# name = "example sub-menu 1" -# parent = "Parent" -# pageRef = "posts" -# weight = 20 - -#[[main]] -# name = "example sub-menu 2" -# parent = "Parent" -# pageRef = "posts" -# weight = 20 - -#[[subnavigation]] -# name = "An interesting topic" -# pageRef = "tags/interesting-topic" -# weight = 10 - -#[[subnavigation]] -# name = "My Awesome Category" -# pre = "github" -# pageRef = "categories/awesome" -# weight = 20 - -# [[main]] -# name = "Resources" -# pageRef = "resources" -# weight = 20 - -#[[main]] -# name = "Tags" -# pageRef = "tags" -# weight = 30 - - -# -- Footer Menu -- -# The footer menu is displayed at the bottom of the page, just before -# the copyright notice. Configure as per the main menu above. - - -# [[footer]] -# name = "Tags" -# pageRef = "tags" -# weight = 10 - -# [[footer]] -# name = "Categories" -# pageRef = "categories" -# weight = 20 diff --git a/config/_default/module.toml b/config/_default/module.toml deleted file mode 100644 index 74f7727..0000000 --- a/config/_default/module.toml +++ /dev/null @@ -1,3 +0,0 @@ -[hugoVersion] - extended = false - min = "0.87.0" diff --git a/hugo.toml b/hugo.toml deleted file mode 100644 index 4e1092f..0000000 --- a/hugo.toml +++ /dev/null @@ -1,105 +0,0 @@ -baseURL = 'https://guhcampos.github.io/' -languageCode = 'en-us' -defaultContentLanguage = 'en' -title = 'Gustavo Campos' - -# Use theme from local directory -themesDir = "themes" -theme = "blowfish" - -enableRobotsTXT = true -summaryLength = 0 - -[module] - [[module.imports]] - path = "blowfish" - disable = false - -[pagination] - pagerSize = 10 - -[taxonomies] - tag = "tags" - category = "categories" - author = "authors" - series = "series" - -[outputs] - home = ["HTML", "RSS", "JSON"] - -[menu] - [[menu.main]] - name = "Blog" - pageRef = "blog" - weight = 10 - [[menu.main]] - name = "Topics" - pageRef = "topics" - weight = 20 - [[menu.main]] - name = "Lists" - pageRef = "lists" - weight = 30 - [[menu.main]] - name = "Resources" - pageRef = "resources" - weight = 40 - [[menu.main]] - name = "Tags" - pageRef = "tags" - weight = 50 - -[params] - description = "Personal website and blog" - mainSections = ["blog"] - - [params.header] - layout = "basic" # options: basic, fixed - showTitle = true - showMenu = true - - [params.footer] - showCopyright = true - showThemeAttribution = true - showAppearanceSwitcher = true - showScrollToTop = true - - [params.homepage] - layout = "page" # options: page, profile, hero, card, background, custom - showRecent = true - showRecentItems = 5 - showMoreLink = true - showMoreLinkDest = "/blog" - - [params.article] - showDate = true - showDateUpdated = true - showAuthor = true - showBreadcrumbs = true - showDraftLabel = true - showEdit = false - showHeadingAnchors = true - showPagination = true - showReadingTime = true - showTableOfContents = true - showTaxonomies = true - showWordCount = true - showComments = false - - [params.list] - showBreadcrumbs = true - showSummary = true - showTableOfContents = true - showTaxonomies = true - groupByYear = true - - [params.sitemap] - excludedKinds = ["taxonomy", "term"] - - [params.search] - enable = true - type = "fuse" - - [params.colorScheme] - default = "dark" - toggle = true diff --git a/hugo/.hugo_build.lock b/hugo/.hugo_build.lock new file mode 100644 index 0000000..e69de29 diff --git a/archetypes/default.md b/hugo/archetypes/default.md similarity index 100% rename from archetypes/default.md rename to hugo/archetypes/default.md diff --git a/hugo/assets/css/custom.css b/hugo/assets/css/custom.css new file mode 100644 index 0000000..fa129dd --- /dev/null +++ b/hugo/assets/css/custom.css @@ -0,0 +1,8 @@ +/* html { + font-size: 10pt; +} + +body * { + text-align: justify; + justify-content: center; +} */ diff --git a/hugo/assets/icons/table-sort.svg b/hugo/assets/icons/table-sort.svg new file mode 100644 index 0000000..a6519ab --- /dev/null +++ b/hugo/assets/icons/table-sort.svg @@ -0,0 +1,6 @@ + + + diff --git a/hugo/config/_default/hugo.toml b/hugo/config/_default/hugo.toml new file mode 100644 index 0000000..584b1ee --- /dev/null +++ b/hugo/config/_default/hugo.toml @@ -0,0 +1,33 @@ +baseURL = 'https://guhcampos.github.io/' +languageCode = 'en' +defaultContentLanguage = 'en' +title = 'Gustavo Campos' +# sectionPagesMenu = "main" + +themesDir = "themes" +theme = "blowfish" + +enableRobotsTXT = true +summaryLength = 0 + +[module] + [[module.imports]] + path = "blowfish" + disable = false + + [[module.mounts]] + source = 'static/playlists' + target = 'data/playlists' + includeFiles = ['*.json'] + +[pagination] + pagerSize = 10 + +[taxonomies] + series = "series" + tag = "tags" + # topic = "topics" + + +[outputs] + home = ["HTML", "RSS", "JSON"] diff --git a/hugo/config/_default/languages.en.toml b/hugo/config/_default/languages.en.toml new file mode 100644 index 0000000..22344f7 --- /dev/null +++ b/hugo/config/_default/languages.en.toml @@ -0,0 +1,19 @@ +contentDir = "content/en" +languageCode = "en" +languageName = "English" +title = "guhcampos" +weight = 1 + +[params] +displayName = "en" +isoCode = "en" +rtl = false +dateFormat = "January 2, 2006" +description = "Guhcampos Digital Garden" + + [params.author] + name = "Gustavo Campos" + email = "guhcampos@gmail.com" + headline = "Brazilian, Millenial, Tinkerer, SCUBA diver, Nerd." + bio = "Brazilian, Millenial, Tinkerer, Nerd. I write about random stuff and for some reason I decided to start publishing it." + diff --git a/hugo/config/_default/languages.pt-br.toml b/hugo/config/_default/languages.pt-br.toml new file mode 100644 index 0000000..bfe0bef --- /dev/null +++ b/hugo/config/_default/languages.pt-br.toml @@ -0,0 +1,18 @@ +contentDir = "content/pt-br" +languageCode = "pt-br" +languageName = "Português" +title = "guhcampos" +weight = 2 + +[params] +displayName = "pt-br" +isoCode = "pt-BR" +rtl = false +dateFormat = "2 January 2006" +description = "Guhcampos Digital Garden" + + [params.author] + name = "Gustavo Campos" + email = "guhcampos@gmail.com" + headline = "Brasileiro, Millenial, Gambiarreiro, Mergulhador, Nerd." + bio = "Escrevo sobre coisas aleatórias e, por algum motivo, decidi começar a publicar." diff --git a/config/_default/markup.toml b/hugo/config/_default/markup.toml similarity index 100% rename from config/_default/markup.toml rename to hugo/config/_default/markup.toml diff --git a/hugo/config/_default/menus.en.toml b/hugo/config/_default/menus.en.toml new file mode 100644 index 0000000..ad5c939 --- /dev/null +++ b/hugo/config/_default/menus.en.toml @@ -0,0 +1,32 @@ +[[main]] +name = "Blog" +pageRef = "posts" +weight = 10 + +[[main]] + name = "Music" + weight = 20 + +[[main]] +name = "Playlists" +parent = "Music" +pageRef = "playlists" +weight = 30 + + +[[main]] +name = "About" +pageRef = "about" +weight = 50 + +# ---------------- + +[[footer]] +name = "Tags" +pageRef = "tags" +weight = 10 + +# [[footer]] +# name = "Topics" +# pageRef = "topics" +# weight = 20 diff --git a/hugo/config/_default/menus.pt-br.toml b/hugo/config/_default/menus.pt-br.toml new file mode 100644 index 0000000..2b1e44b --- /dev/null +++ b/hugo/config/_default/menus.pt-br.toml @@ -0,0 +1,31 @@ +[[main]] +name = "Blog" +pageRef = "posts" +weight = 10 + +[[main]] + name = "Música" + weight = 20 + +[[main]] +name = "Playlists" +parent = "Música" +pageRef = "playlists" +weight = 20 + +[[main]] +name = "Sobre" +pageRef = "about" +weight = 50 + +# ---------------- + +[[footer]] +name = "Tags" +pageRef = "tags" +weight = 10 + +# [[footer]] +# name = "Tópicos" +# pageRef = "topics" +# weight = 20 diff --git a/hugo/config/_default/module.toml b/hugo/config/_default/module.toml new file mode 100644 index 0000000..5941348 --- /dev/null +++ b/hugo/config/_default/module.toml @@ -0,0 +1,38 @@ +[hugoVersion] + extended = false + min = "0.148.0" + +[[module.imports]] + path = "blowfish" + disable = false + +[[module.mounts]] +source = 'static/playlists' +target = 'data/playlists' +includeFiles = ['*.json'] + +[[module.mounts]] +source = 'content/en' +target = 'content' +lang = 'en' + +[[module.mounts]] +source = 'content/pt' +target = 'content' +lang = 'pt' + +[[module.mounts]] +source = 'content/en' +target = 'content' +lang = 'pt' + + +[[module.mounts]] + source = "build/obsidian/en/posts" + target = "content/en/posts" + includeFiles = ['*.md'] + +[[module.mounts]] + source = "build/obsidian/pt/posts]" + target = "content/pt/posts" + includeFiles = ['*.md'] diff --git a/config/_default/params.toml b/hugo/config/_default/params.toml similarity index 71% rename from config/_default/params.toml rename to hugo/config/_default/params.toml index 815f1e4..e5460ab 100644 --- a/config/_default/params.toml +++ b/hugo/config/_default/params.toml @@ -1,8 +1,8 @@ -colorScheme = "blowfish" +colorScheme = "ocean" defaultAppearance = "light" autoSwitchAppearance = true enableSearch = true -enableCodeCopy = false +enableCodeCopy = true replyByEmail = false mainSections = [ "Resources", "Topics", "Lists", "Tutorials" ] disableImageOptimization = false @@ -18,9 +18,20 @@ verification = { } rssnext = { } highlightCurrentMenuArea = "" smartTOC = true +[author] + +[[author.links]] +github = "https://github.com/guhcampos" +gitlab = "https://gitlab.com/guhcampos" +instagram = "https://www.instagram.com/guhcampos.stuff/" +lastfm = "https://www.last.fm/user/guhcampos" +linkedin = "https://www.linkedin.com/in/gucampos/" +reddit = "https://www.reddit.com/user/guhcampos/" +spotify = "https://open.spotify.com/user/12132336829" +steam = "https://steamcommunity.com/id/guhcampos/" [header] -layout = "basic" +layout = "fixed-fill-blur" [footer] showMenu = true @@ -40,33 +51,33 @@ cardViewScreenWidth = false layoutBackgroundBlur = false [article] -showDate = true -showViews = false -showLikes = false -showDateOnlyInArticle = false -showDateUpdated = false -showAuthor = true -showHero = false +editAppendPath = true +invertPagination = false layoutBackgroundBlur = true layoutBackgroundHeaderSpace = true +seriesOpened = true +showAuthor = false +showAuthorsBadges = true showBreadcrumbs = false +showCategoryOnly = false +showDate = true +showDateOnlyInArticle = true +showDateUpdated = true showDraftLabel = true showEdit = false -editAppendPath = true -seriesOpened = false showHeadingAnchors = true +showHero = true +showLikes = false showPagination = true -invertPagination = false showReadingTime = true showTableOfContents = false -showTaxonomies = false -showCategoryOnly = false -showAuthorsBadges = false +showTaxonomies = true +showViews = false showWordCount = true showZenMode = false [list] -showHero = false +showHero = true layoutBackgroundBlur = true layoutBackgroundHeaderSpace = true showBreadcrumbs = false @@ -74,10 +85,10 @@ showSummary = false showViews = false showLikes = false showTableOfContents = false -showCards = false +showCards = true orderByWeight = false groupByYear = true -cardView = false +cardView = true cardViewScreenWidth = false constrainItemsWidth = false @@ -86,7 +97,7 @@ excludedKinds = [ "taxonomy", "term" ] [taxonomy] showTermCount = true -showHero = false +showHero = true showBreadcrumbs = false showViews = false showLikes = false @@ -94,7 +105,7 @@ showTableOfContents = false cardView = false [term] -showHero = false +showHero = true showBreadcrumbs = false showViews = false showLikes = false diff --git a/hugo/content/_index.md b/hugo/content/_index.md new file mode 100644 index 0000000..177d4a8 --- /dev/null +++ b/hugo/content/_index.md @@ -0,0 +1,7 @@ +--- +showDate: false +showAuthor: false +showReadingTime: false +showWordCount: false +# showRecent: false +--- diff --git a/hugo/content/en/about.md b/hugo/content/en/about.md new file mode 100644 index 0000000..ee04889 --- /dev/null +++ b/hugo/content/en/about.md @@ -0,0 +1,8 @@ +--- +title: About Me +showDate: false +showAuthor: false +showReadingTime: false +showWordCount: false + +--- diff --git a/hugo/content/en/playlists/_content.gotmpl b/hugo/content/en/playlists/_content.gotmpl new file mode 100644 index 0000000..f661e3f --- /dev/null +++ b/hugo/content/en/playlists/_content.gotmpl @@ -0,0 +1,37 @@ +{{ .EnableAllLanguages }} +{{ $playlists := .Site.Data.playlists }} +{{ range $playlist_id, $playlist_data := $playlists }} + + {{ $content := dict + "mediaType" "text/markdown" + "value" "" + }} + + {{ $dates := dict "date" (time.AsTime "2024-01-01") }} + + {{ $total_duration := 0 }} + {{ range $playlist_data.tracks }} + {{ $total_duration = add $total_duration .duration_ms }} + {{ end }} + + {{ $params := dict + "playlist_id" $playlist_id + "title" $playlist_data.name + "description" $playlist_data.description + "tracks_count" (len $playlist_data.tracks) + "duration_ms" $total_duration + "tracks" $playlist_data.tracks + "url" $playlist_data.url + }} + + {{ $page := dict + "content" $content + "dates" $dates + "kind" "page" + "params" $params + "path" $playlist_id + "title" $playlist_data.name + }} + {{ $.AddPage $page }} + +{{ end }} diff --git a/hugo/content/en/playlists/_index.md b/hugo/content/en/playlists/_index.md new file mode 100644 index 0000000..11e1c10 --- /dev/null +++ b/hugo/content/en/playlists/_index.md @@ -0,0 +1,32 @@ +--- +title: Music Playlists +groupByYear: false +cascade: + params: + + showRecent: false + showAuthor: false + showDate: false + showWordCount: false + showReadingTime: false + +tags: + - music +topics: + - music +--- + +I'm a shuffle type of music listener, and as such I make heavy use of playlists +to mix and match music I like and listen it for long periods of time. This is a +list of playlists I have created over time that may be interesting to someone +else out there. + +Having them here also helps me not be a prisioner of any one music listening +service: as I have them all in JSON and M3U formats, I can easily migrate them +between different services if I so wish to. + +These playlists are exported directly from my [Spotify account](https://open.spotify.com/user/12132336829) +where you can find quite a few more, some receive more love than others. + +I'm always open to getting music recommendations, so if you want to share something, +let me know! diff --git a/hugo/content/en/posts/_index.md b/hugo/content/en/posts/_index.md new file mode 100644 index 0000000..e69de29 diff --git a/hugo/content/en/posts/pairing-with-ai-to-design-this-website-part-01-first-steps.md b/hugo/content/en/posts/pairing-with-ai-to-design-this-website-part-01-first-steps.md new file mode 100644 index 0000000..0ba96a9 --- /dev/null +++ b/hugo/content/en/posts/pairing-with-ai-to-design-this-website-part-01-first-steps.md @@ -0,0 +1,103 @@ +--- +date: 2025-07-16 +title: Pairing with AI to Design This Website Part 01 - First Steps +tags: +- tech +- web-development +- ai +- software +- engineering + +--- +## Everything Begins with an Idea + +I'm a bit of a low-profile person on the Internet, which seems to be bad for your career these days, so after 15+ years without having a personal website or blog I decided that it was time to have one again. It does not help that social networks are in a bad shape in 2025, so having my own place to share some ideas and updates does not hurt either, so I started to build one. + +I had a few initial requirements, the first one being: I use [Obsidian](https://obsidian.md/) for my notes and would like to publish some of my Obsidian content into my website. I could use [Obsidian Sync](https://obsidian.md/sync) for that, but being in Brazil, adding expenses in USD to my budget is often a no-no. Besides, I wanted to have a bit of personality built into this, so I wanted a Static Site Generator to help me build the way I wanted. Having had some previous experience with [Hugo](https://gohugo.io/) a long time ago, I went wit it. Chose a [nice feature-rich theme](https://blowfish.page/) and fired VS Code. + +Then I froze. + +I'm a backend-devops-infra-data guy, and I haven't played with frontend code for a long, long time. I used to write a lot of HTML and CSS when [JQuery](https://jquery.com/) and [Lodash](https://lodash.com/) were the gold standard, but opening my theme's code felt completely alien to me. I'm sure there must be a reason why frontend folks love [TailwindCSS](https://tailwindcss.com/), but to my unaccustomed eyes that felt too big a grammar to learn just to put a website up. There was no way I was going to understand this just to build a blog: + +```html + +``` + +Then it came to me: _I'm going to AI the shit out of this._ + +I've been very slow to jump into the AI hype, as I do with most hype. After the first year or so of revolutionary claims and heated politics, the actual decent use cases started to emerge and I started to get curious about it. I tried generating some code here and there and found it to be lacking a bit compared to my own code skills - but here we are, faced with code I suck at writing, so why not give it a chance? + +So here we are. My very first article series in this website will be my attempt to log my process pairing with an AI to build this very website you're hopefully reading this article on the future. By process I mean obviously the successes and frustrations, and if the frustrations outweigh the successes too much there's a very real chance you'll never get to read this and I'm just talking to myself as usual. + +## Setting up + +I already had a stack to work on: **Hugo** to generate a static website and **Obsidian** as my main editor, so **Markdown** would be my content format of choice. I chose [Cursor](https://cursor.com/)as my editor and would leverage my Python background to write the code I needed to feed Hugo with stuff. + +First step, let's bring some Obsidian stuff into Hugo. I started writing a simple script to copy Obsidian Markdown files to my Hugo content directory, but this needed a bit of translation. Nothing complicated, but a bit boring, so I invited Cursor's agent for a chat and asked it to do it (sorry, I lost that prompt, it was before I thought about making this into a blog post) and it came up with [this script](https://github.com/guhcampos/guhcampos.github.io/blob/ab861cf75d9ae3025d6d66da1c33d4aaee21e108/scripts/sync-obsidian) + +As many before me, I got a little offended by its eagerness to over-explain every line of code: + +```python +def process_file(file_path: Path): + """Process a single markdown file.""" + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + frontmatter, content = extract_frontmatter(content) + + # Skip if not marked for publishing + if not frontmatter.get('publish', False): + return + + # Convert content + content = convert_obsidian_links(content) + + # Determine the section + section = determine_section(file_path) + + # Clean up the title + title = frontmatter.get('title', clean_title(file_path.stem)) + + # Prepare Hugo frontmatter + hugo_frontmatter = { + 'title': title, + 'date': frontmatter.get('date', datetime.now().strftime('%Y-%m-%d')), + 'lastmod': datetime.now().strftime('%Y-%m-%d'), + 'draft': False + } + ... +``` + +The whole code is a bit too verbose to my taste, and not very well structured, but it did split the code into functions that made sense, and most importantly: **it worked** the first time. Kudos for my new AI pair! + +I wanted to make this repository public as a way to showcase my interactions with the AI, so I made sure to scrub any secret information from it. That of course meant my Obsidian content could not live in the same repository as my website. I already used the great [Obsidian Git](https://github.com/Vinzent03/obsidian-git)to backup my vault to a private Github repository, so why not leverage that? + +I then asked the AI to create a Github Actions pipeline to fetch the vault data on build time, directly from the repo, and to my actual surprise, the [generated workflow](https://github.com/guhcampos/guhcampos.github.io/blob/ab861cf75d9ae3025d6d66da1c33d4aaee21e108/.github/workflows/hugo.yml) also did work pretty much the first time. But then the first concerns started. Let's take this snippet as an example: + +```yaml + - name: Setup Hugo + uses: peaceiris/actions-hugo@v2 + with: + hugo-version: '0.147.7' + extended: true +``` + +The AI has added a third party Github action into my pipeline, which begs the question: should I trust it? A well informed developer must be aware of [Supply Chain Attacks](https://www.cloudflare.com/learning/security/what-is-a-supply-chain-attack/) and this is screaming Supply Chain attack to me. Who is [peaceiris](https://github.com/peaceiris)? Is that a trusted developer? Is that even a person or a company? What happens if they decide to add malicious code to this action and send all my precious Obsidian Vault data to some third party? + +## Learning a Bunch of Lessons in a Short Time + +Ok, time to stop for a second and evaluate options. **First lesson learned: don't trust AI generated code without a very broad and well informed code review**. (that should probably become its own blog article!) + +So I did we all technologists do and did a bit of due diligence. Seems like [peaceiris/actions-hugo](https://github.com/peaceiris/actions-hugo)is quite widely used, and has been [consistently releasing trustworthy code](https://github.blog/developer-skills/github/github-action-hero-shohei-ueda/) for six years. It would be quite a bit of work to write my own Github Action for this simple use case, so I decided to trust this and go with the AI generated code. **Ship It!** + +But wait, during my due diligence I noted something interesting. Note how the generated code uses `actions-hugo@v2`? Well, this is June 2025 and `actions-hugo@v3` seems to have been released in [April 2024](https://github.com/peaceiris/actions-hugo/releases/tag/v3.0.0) we're over a year out of date from their latest version! Whatever data this model (I think this was `claude-4-sonnet`) was trained on was really, really old. So let's bump this to `v3` before we use it in the real world, shall we? + +**Second lesson learned: AI models are trained on data from the past, so take that into account when doing your code review and make sure dependencies and syntax are up to date** + +Finally, I was getting uncomfortable with the code quality in this Python script. I have big plans for it besides just copying my Obsidian content into Hugo, so it needed a bit more structure to grow, so I jumped in and refactored it all into a [Click](https://click.palletsprojects.com/en/stable/) tool I could then easily extend beyond its initial abilities. In the meantime, I got to read and understand the code generated by the AI and make some optimizations. All said and done, I think I spent about the same time writing as I would have if starting from scratch, but with a big, big difference: I actually wrote it, while otherwise _I would have actually procrastinated the moment I froze looking at the TailwindCSS code_. + +Notice how I didn't even mention the frontend code in this post? I didn't have to do much to put a [basic placeholder homepage](https://blowfish.page/docs/homepage-layout/) up. So I didn't really need the AI for that, but having it made me go **from zero to something** really, really fast, and that gave me traction to actually continue on my journey, overcoming the frustration of hitting an initial wall. **Third lesson learned: AI coding is great for prototyping and putting something out there, even if it's not the definitive version you want to stick to** + +I think 3 is a good short number to start with. Three lessons learned in a day of play, so three lessons passed forward with this little blog post. Until next time! \ No newline at end of file diff --git a/hugo/content/pt-br/about.md b/hugo/content/pt-br/about.md new file mode 100644 index 0000000..ffccb25 --- /dev/null +++ b/hugo/content/pt-br/about.md @@ -0,0 +1,7 @@ +--- +title: Sobre Mim +showDate: false +showAuthor: false +showReadingTime: false +showWordCount: false +--- diff --git a/hugo/content/pt-br/playlists/_index.md b/hugo/content/pt-br/playlists/_index.md new file mode 100644 index 0000000..41d5e8a --- /dev/null +++ b/hugo/content/pt-br/playlists/_index.md @@ -0,0 +1,28 @@ +--- +title: Playlists + +cascade: + params: + showRecent: false + showAuthor: false + showDate: false + showWordCount: false + showReadingTime: false + +tags: + - music +topics: + - music +--- + +A vida inteira eu escutei música no shuffle. Salvo raros álbums que precisam ser +escutados em sua *completude*, eu prefiro ouvir minhas músicas favoritas de +múltiplos artistas do que maratonar um único artista o dia todo. Por conta disso, +tenho o hábito de criar playlists com certas temáticas específicas pra momentos +específicos, e decidi que era uma boa ideia compartilhar com todo mundo. + +Uma vantagem de ter essas playlists exportadas aqui é que eu passo a ter esses +dados em formato JSON e M3U, facilitando a importação desses dados em outras +plataformas, assim não fico preso a uma plataforma tipo Spotify, impedido de +mudar pra não perder minhas amadas playlists. + diff --git a/hugo/content/pt-br/posts/_index.md b/hugo/content/pt-br/posts/_index.md new file mode 100644 index 0000000..e69de29 diff --git a/hugo/data/playlists/.gitkeep b/hugo/data/playlists/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/hugo/layouts/partials/article-link/playlist-card.html b/hugo/layouts/partials/article-link/playlist-card.html new file mode 100644 index 0000000..8aa0df1 --- /dev/null +++ b/hugo/layouts/partials/article-link/playlist-card.html @@ -0,0 +1,70 @@ +{{ $disableImageOptimization := .Page.Site.Params.disableImageOptimization | default false }} +{{ $playlist_duration_ms := 0 }} +{{ range $.Params.tracks }} + {{ $playlist_duration_ms = add $playlist_duration_ms .duration_ms }} +{{ end }} +{{ $playlist_duration := div $playlist_duration_ms 60000 | int }} + + +
+ + {{- with $.Params.images -}} + {{- range first 6 . }} + {{ end -}} + {{- else -}} + {{- $images := $.Resources.ByType "image" -}} + {{- $featured := $images.GetMatch "*feature*" -}} + {{- if not $featured }}{{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}{{ end -}} + {{ if and .Params.featureimage (not $featured) }} + {{- $url:= .Params.featureimage -}} + {{ $featured = resources.GetRemote $url }} + {{ end }} + {{- if not $featured }}{{ with .Site.Params.defaultFeaturedImage }}{{ $featured = resources.Get . }}{{ end }}{{ end -}} + {{ if .Params.hideFeatureImage }}{{ $featured = false }}{{ end }} + {{- with $featured -}} + {{ if or $disableImageOptimization (strings.HasSuffix $featured ".svg")}} + {{ with . }} +
+ {{ end }} + {{ else }} + {{ with .Resize "600x" }} +
+ {{ end }} + {{ end }} + {{- else -}} + {{- with $.Site.Params.images }} + {{ end -}} + {{- end -}} + {{- end -}} + +
+
+ {{ .Title }} +
+ +
+ {{ $.Params.description }} +
+ +
+ + + + + + {{ len $.Params.tracks }} {{ i18n "playlists.tracks" | default "tracks" }} + + + + + + + {{ $playlist_duration }} {{ i18n "playlists.minutes" | default "min" }} + +
+
+ {{ partial "article-meta/basic.html" . }} +
+
+
+
diff --git a/hugo/layouts/partials/playlists/playlist-table.html b/hugo/layouts/partials/playlists/playlist-table.html new file mode 100644 index 0000000..cc9439a --- /dev/null +++ b/hugo/layouts/partials/playlists/playlist-table.html @@ -0,0 +1,202 @@ + + + + + + + + + + + +{{ range $index, $track := $.Params.tracks }} {{ +$track_duration_seconds := div .duration_ms 1000 }} {{ $minutes := +div $track_duration_seconds 60 | int }} {{ $seconds := mod +$track_duration_seconds 60 | int }} + + + + + + +{{ end }} + +
+

+ {{ i18n "playlists.title" | default "Title" }} + {{/* {{ partial "icon.html" "table-sort" }} */}} +

+
+

+ {{ i18n "playlists.artist" | default "Artist" }} + {{/* {{ partial "icon.html" "table-sort" }} */}} +

+
+

+ {{ i18n "playlists.album" | default "Album" }} + {{/* {{ partial "icon.html" "table-sort" }} */}} +

+
+

+ {{ i18n "playlists.duration" | default "Time" }} + {{/* {{ partial "icon.html" "table-sort" }} */}} +

+
+
+ {{ .name }} +
+
+
+ {{ index .artists 0 }} +
+
+
+ {{ .album }} +
+
+ {{ printf "%d:%02d" $minutes $seconds }} +
+ diff --git a/hugo/layouts/playlists/list.html b/hugo/layouts/playlists/list.html new file mode 100644 index 0000000..a311594 --- /dev/null +++ b/hugo/layouts/playlists/list.html @@ -0,0 +1,122 @@ +{{ define "main" }} + +{{ .Scratch.Set "scope" "list" }} +{{ if .Site.Params.list.showHero | default false }} +{{ $heroStyle := print "hero/" .Site.Params.list.heroStyle ".html" }} +{{ if templates.Exists ( printf "partials/%s" $heroStyle ) }} +{{ partial $heroStyle . }} +{{ else }} +{{ partial "hero/basic.html" . }} +{{ end }} +{{- end -}} + +
+ {{ if .Params.showBreadcrumbs | default (.Site.Params.list.showBreadcrumbs | default false) }} + {{ partial "breadcrumbs.html" . }} + {{ end }} +

{{ .Title }}

+
+ {{ partial "article-meta/list.html" (dict "context" . "scope" "single") }} +
+ {{ $translations := .AllTranslations }} + {{ with .File }} + {{ $path := .Path }} + {{range $translations}} + {{ $lang := print "." .Lang ".md" }} + {{ $path = replace $path $lang ".md" }} + {{end}} + {{ $jsPage := resources.Get "js/page.js" }} + {{ $jsPage = $jsPage | resources.Minify | resources.Fingerprint ($.Site.Params.fingerprintAlgorithm | default "sha512") }} + + {{ end }} +
+
+
+ {{ .Content }} +
+
+ {{ if gt .Pages 0 }} + + {{ $cardView := .Params.cardView | default (.Site.Params.list.cardView | default false) }} + {{ $cardViewScreenWidth := .Params.cardViewScreenWidth | default (.Site.Params.list.cardViewScreenWidth | default false) }} + {{ $groupByYear := .Params.groupByYear | default ($.Site.Params.list.groupByYear | default false) }} + {{ $orderByWeight := .Params.orderByWeight | default ($.Site.Params.list.orderByWeight | default false) }} + {{ $groupByYear := and (not $orderByWeight) $groupByYear }} + + {{ if not $cardView }} + +
+ {{ if not $orderByWeight }} + {{ range (.Paginate (.Pages.GroupByDate "2006")).PageGroups }} + {{ if $groupByYear }} +

+ {{ .Key }} +

+ {{ end }} + {{ range .Pages }} + {{ partial "article-link/simple.html" . }} + {{ end }} + {{ end }} + {{ else }} + {{ range (.Paginate (.Pages.ByWeight)).Pages }} + {{ partial "article-link/simple.html" . }} + {{ end }} + {{ end }} +
+ + {{ else }} + + {{ if $groupByYear }} + + {{ range (.Paginate (.Pages.GroupByDate "2006")).PageGroups }} + {{ if $cardViewScreenWidth }} +
+ {{ end }} +

+ {{ .Key }} +

+
+ {{ range .Pages }} + {{ partial "article-link/playlist-card.html" . }} + {{ end }} +
+ {{ if $cardViewScreenWidth }}
{{ end }} + {{ end }} + + {{ else }} + + {{ if $cardViewScreenWidth }} +
+
+ {{ else }} +
+ {{ end }} + {{ if not $orderByWeight }} + {{ range (.Paginate (.Pages.GroupByDate "2006")).PageGroups }} + {{ range .Pages }} + {{ partial "article-link/playlist-card.html" . }} + {{ end }} + {{ end }} + {{ else }} + {{ range (.Paginate (.Pages.ByWeight)).Pages }} + {{ partial "article-link/playlist-card.html" . }} + {{ end }} + {{ end }} +
+ {{ if $cardViewScreenWidth }}
{{ end }} + + {{ end }} + + {{end}} + + {{ else }} +
+

+ {{ i18n "list.no_articles" | emojify }} +

+
+ {{ end }} + + {{ partial "pagination.html" . }} + + {{ end }} diff --git a/hugo/layouts/playlists/single.html b/hugo/layouts/playlists/single.html new file mode 100644 index 0000000..431699b --- /dev/null +++ b/hugo/layouts/playlists/single.html @@ -0,0 +1,120 @@ +{{ define "main" }} +{{ .Scratch.Set "scope" "single" }} + +{{ $total_duration := 0 }} +{{ range .Params.tracks }} +{{ $total_duration = add $total_duration .duration_ms }} +{{ end }} +{{ $duration_minutes := div $total_duration 60000 | int }} + + +
+ +
+

+ {{ .Title | emojify }} +

+
+ {{ partial "article-meta/basic.html" (dict "context" . "scope" "single") }} +
+ + {{ $authorsData := .Site.Data.authors }} + {{ $taxonomies := .Site.Taxonomies.authors }} + {{ $baseURL := .Site.BaseURL }} + {{ $taxonomyLink := 0 }} + {{ $showAuthor := 0 }} + + {{ if not (strings.HasSuffix $baseURL "/") }} + {{ $baseURL = delimit (slice $baseURL "/") "" }} + {{ end }} + + {{ if not (.Params.showAuthorBottom | default ( .Site.Params.article.showAuthorBottom | default false)) }} + + {{ if .Params.showAuthor | default (.Site.Params.article.showAuthor | default true) }} + {{ $showAuthor = 1 }} + {{ partial "author.html" . }} + {{ end }} + + {{ range $author := .Page.Params.authors }} + {{ $authorData := index $authorsData $author }} + {{- if $authorData -}} + {{ range $taxonomyname, $taxonomy := $taxonomies }} + {{ if (eq $taxonomyname $author) }} + {{ $taxonomyLink = delimit (slice $baseURL "authors/" $author "/") "" }} + {{ end }} + {{ end }} + + {{ $finalLink := $taxonomyLink }} + {{ $currentLang := $.Site.Language.Lang }} + {{ if eq $.Site.LanguagePrefix "" }} + {{ $finalLink = printf "%sauthors/%s/" $baseURL $author }} + {{ else }} + {{ $finalLink = printf "%s%s/authors/%s/" $baseURL $currentLang $author }} + {{ end }} + + {{ partial "author-extra.html" (dict "context" . "data" $authorData "link" $finalLink) }} + {{- end -}} + {{ end }} + + {{ if or $taxonomyLink $showAuthor }} +
+ {{ end }} + + {{ end }} + +
+ +
+ +
+ + {{ partial "series/series.html" . }} + +
+ {{ .Params.description | safeHTML }} +
+ + + + +
+
+ {{ partial "playlists/playlist-table.html" . }} +
+
+ + {{ partial "series/series-closed.html" . }} + {{ partial "sharing-links.html" . }} + {{ partial "related.html" . }} + +
+ + {{ $translations := .AllTranslations }} + {{ with .File }} + {{ $path := .Path }} + {{ range $translations }} + {{ $lang := print "." .Lang ".md" }} + {{ $path = replace $path $lang ".md" }} + {{ end }} + {{ $jsPage := resources.Get "js/page.js" }} + {{ $jsPage = $jsPage | resources.Minify | resources.Fingerprint ($.Site.Params.fingerprintAlgorithm | default "sha512") }} + + {{ end }} + +
+
+{{ end }} diff --git a/hugo/static/.gitkeep b/hugo/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/hugo/themes/blowfish b/hugo/themes/blowfish new file mode 160000 index 0000000..0d485fc --- /dev/null +++ b/hugo/themes/blowfish @@ -0,0 +1 @@ +Subproject commit 0d485fcbf34acd8ff21a4b58fc20399b86b8c46d diff --git a/layouts/partials/under-construction.html b/layouts/partials/under-construction.html deleted file mode 100644 index e98d3e6..0000000 --- a/layouts/partials/under-construction.html +++ /dev/null @@ -1,15 +0,0 @@ -{{/* Under Construction Banner */}} -
-
-
- - - -

- Under Construction - -

-
-
-
-
diff --git a/layouts/resources/list.html b/layouts/resources/list.html deleted file mode 100644 index 6c59de5..0000000 --- a/layouts/resources/list.html +++ /dev/null @@ -1,40 +0,0 @@ -{{ define "main" }} -
-
- {{ if .Title }} -

- {{ .Title }} -

- {{ end }} -
- {{ .Description | markdownify }} -
-
- - {{/* Display pages in a grid */}} -
- {{ range .Pages }} -
-

- {{ .Title }} -

- {{ with .Description }} -

- {{ . }} -

- {{ end }} - {{ with .Params.tags }} -
- {{ range . }} - - #{{ . }} - - {{ end }} -
- {{ end }} -
- {{ end }} -
-
-{{ end }} diff --git a/layouts/resources/section.html b/layouts/resources/section.html deleted file mode 100644 index 24fa327..0000000 --- a/layouts/resources/section.html +++ /dev/null @@ -1,89 +0,0 @@ -{{ define "main" }} -
-
- {{ if .Title }} -

- {{ .Title }} -

- {{ end }} -
- {{ .Description | markdownify }} -
-
- - {{/* Get all pages and group them by category */}} - {{ $pages := where .Pages "Kind" "page" }} - {{ $categories := slice }} - {{ range $pages }} - {{ with .Params.categories }} - {{ range . }} - {{ $categories = $categories | append . }} - {{ end }} - {{ end }} - {{ end }} - {{ $uniqueCategories := uniq $categories }} - - {{/* Display resources grouped by category */}} - {{ range $category := $uniqueCategories }} -
-

{{ $category }}

-
- {{ range $pages }} - {{ if in .Params.categories $category }} -
-

- {{ .Title }} -

- {{ with .Description }} -

- {{ . }} -

- {{ end }} - {{ with .Params.tags }} -
- {{ range . }} - - {{ . }} - - {{ end }} -
- {{ end }} -
- {{ end }} - {{ end }} -
-
- {{ end }} - - {{/* Display uncategorized resources if any */}} - {{ $uncategorized := where $pages "Params.categories" nil }} - {{ with $uncategorized }} -
-

Other Resources

-
- {{ range . }} -
-

- {{ .Title }} -

- {{ with .Description }} -

- {{ . }} -

- {{ end }} - {{ with .Params.tags }} -
- {{ range . }} - - {{ . }} - - {{ end }} -
- {{ end }} -
- {{ end }} -
-
- {{ end }} -
-{{ end }} diff --git a/layouts/shortcodes/render-partial.html b/layouts/shortcodes/render-partial.html deleted file mode 100644 index c9d3496..0000000 --- a/layouts/shortcodes/render-partial.html +++ /dev/null @@ -1 +0,0 @@ -{{ partial (.Get 0) . }} diff --git a/layouts/tags/list.html b/layouts/tags/list.html deleted file mode 100644 index 1fee7f6..0000000 --- a/layouts/tags/list.html +++ /dev/null @@ -1,82 +0,0 @@ -{{ define "main" }} -
-
-

- Tags -

-
- Browse content by tags -
-
- - {{/* Get all tags and sort them */}} - {{ $tags := slice }} - {{ range .Site.Pages }} - {{ with .Params.tags }} - {{ range . }} - {{ $tags = $tags | append . }} - {{ end }} - {{ end }} - {{ end }} - {{ $uniqueTags := uniq $tags | sort }} - - {{/* Display tag cloud */}} -
- {{ range $uniqueTags }} - {{ $tag := . }} - {{ $count := 0 }} - {{ range $.Site.Pages }} - {{ if in .Params.tags $tag }} - {{ $count = add $count 1 }} - {{ end }} - {{ end }} - - {{ . }} ({{ $count }}) - - {{ end }} -
- - {{/* Display content grouped by tags */}} - {{ range $uniqueTags }} -
-

- {{ . }} -

-
- {{ range $.Site.Pages }} - {{ if in .Params.tags $ }} -
-

- {{ .Title }} -

- {{ with .Description }} -

- {{ . }} -

- {{ end }} -
- {{ with .Section }} - - {{ . }} - - {{ end }} - {{ with .Params.categories }} - {{ range . }} - - {{ . }} - - {{ end }} - {{ end }} -
-
- {{ end }} - {{ end }} -
-
- {{ end }} -
-{{ end }} diff --git a/layouts/tags/term.html b/layouts/tags/term.html deleted file mode 100644 index a83a80b..0000000 --- a/layouts/tags/term.html +++ /dev/null @@ -1,41 +0,0 @@ -{{ define "main" }} -
-
-

- Tag: {{ .Title }} -

-
- Content tagged with "{{ .Title }}" -
-
- -
- {{ range .Pages }} -
-

- {{ .Title }} -

- {{ with .Description }} -

- {{ . }} -

- {{ end }} -
- {{ with .Section }} - - {{ . }} - - {{ end }} - {{ with .Params.categories }} - {{ range . }} - - {{ . }} - - {{ end }} - {{ end }} -
-
- {{ end }} -
-
-{{ end }} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d49665b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,119 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "guhcampos" +version = "0.1.0" +description = "Build tools for guhcampos.github.io" +readme = "README.md" +requires-python = ">=3.12" +license = {text = "MIT"} +authors = [ + {name = "Gustavo Campos", email = "guhcampos@gmail.com"}, +] +keywords = ["hugo", "website", "build", "automation"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Internet :: WWW/HTTP :: Site Management", + "Topic :: Software Development :: Build Tools", +] + +dependencies = [ + "click>=8.1.0", + "rich>=13.0.0", + "pathspec>=0.11.0", + "watchdog>=3.0.0", + "requests>=2.31.0", + "pydantic-settings>=2.10.1", + "python-slugify>=8.0.4", + "pydantic>=2.11.7", + "pyyaml>=6.0.2", + "loguru>=0.7.3", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "black>=23.0.0", + "isort>=5.12.0", + "mypy>=1.0.0", + "ruff>=0.1.0", +] + +[project.scripts] +guhcampos = "guhcampos.cli:guhcampos" + +[project.urls] +Homepage = "https://github.com/guhcampos/guhcampos.github.io" +Repository = "https://github.com/guhcampos/guhcampos.github.io" + +[tool.hatch.build.targets.wheel] +packages = ["src/guhcampos"] + +[tool.black] +line-length = 88 +target-version = ['py312'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["guhcampos"] + +[tool.mypy] +python_version = "3.12" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +[tool.ruff] +target-version = "py312" +line-length = 88 +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[dependency-groups] +dev = [ + "pytest>=8.4.1", +] diff --git a/scripts/sync-obsidian b/scripts/sync-obsidian deleted file mode 100755 index 20153e7..0000000 --- a/scripts/sync-obsidian +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import os -import re -import shutil -from datetime import datetime -from pathlib import Path -from typing import Optional, Set - -import yaml - - -# Configure logging -def setup_logging(): - """Configure logging with both file and console handlers.""" - log_dir = Path("logs") - log_dir.mkdir(exist_ok=True) - - # Create formatters - console_formatter = logging.Formatter( - "%(asctime)s - %(levelname)s - %(message)s", datefmt="%H:%M:%S" - ) - file_formatter = logging.Formatter( - "%(asctime)s - %(levelname)s [%(filename)s:%(lineno)d] - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - # Console handler with colors - console_handler = logging.StreamHandler() - console_handler.setFormatter(console_formatter) - console_handler.setLevel(logging.INFO) - - # File handler for detailed logging - file_handler = logging.FileHandler( - log_dir / f"sync-obsidian-{datetime.now().strftime('%Y%m%d-%H%M%S')}.log" - ) - file_handler.setFormatter(file_formatter) - file_handler.setLevel(logging.DEBUG) - - # Setup root logger - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - root_logger.addHandler(console_handler) - root_logger.addHandler(file_handler) - - return root_logger - - -HUGO_CONTENT = Path(os.getenv("HUGO_CONTENT", "")) -OBSIDIAN_VAULT = Path(os.getenv("OBSIDIAN_VAULT", "")) -SECTION_MAPS = { - "01 Resources": "resources", # Resources - "02 Topics": "topics", # Documentation/Knowledge base - "03 Writing": "blog", # Blog posts - "10 Lists": "lists", # Lists -} - - -def clean_title(title): - """ - Remove numeric prefixes and clean up title. - - My Obsidian vault is organized using a variation of Johnny Decimal, so to - publish it on a friendly way we want to remove the number indexes from the - notes. - """ - # - title = re.sub(r"^\d+(\.\d+)*\s*", "", title) - return title.strip() - - -def clean_path(path): - """Clean path components for URLs.""" - # Remove numeric prefixes from path components and convert to lowercase - parts = [clean_title(part).lower().replace(" ", "-") for part in path.parts] - return Path(*parts) - - -def determine_section(file_path: Path) -> str: - """Determine the section based on the file path.""" - relative_path = file_path.relative_to(OBSIDIAN_VAULT) - first_dir: Optional[str] = ( - relative_path.parts[0] if len(relative_path.parts) > 1 else None - ) - - # Get the mapped section or use a default - return SECTION_MAPS.get(str(first_dir) if first_dir else "", "pages") - - -def is_published(file_path: Path) -> bool: - """Check if a file is marked for publishing.""" - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - - frontmatter, _ = extract_frontmatter(content) - return frontmatter.get("publish", False) - - -def find_published_notes(directory: Path) -> Set[Path]: - """Find all published notes in a directory and its subdirectories.""" - published = set() - for file_path in directory.rglob("*.md"): - if ".trash" not in str(file_path) and ".git" not in str(file_path): - if is_published(file_path): - published.add(file_path) - return published - - -def process_index_file(source_path: Path, dest_path: Path): - """Process an index file (folder description) from Obsidian to Hugo.""" - with open(source_path, "r", encoding="utf-8") as f: - content = f.read() - - frontmatter, content = extract_frontmatter(content) - - # If no frontmatter exists, create one with the title - if not frontmatter: - frontmatter = {"title": clean_title(source_path.parent.name), "draft": False} - - # Convert content - content = convert_obsidian_links(content) - - # Create Hugo content - hugo_content = f"---\n{yaml.dump(frontmatter)}---\n\n{content}" - - # Create parent directories if they don't exist - dest_path.parent.mkdir(parents=True, exist_ok=True) - - # Write the file - with open(dest_path, "w", encoding="utf-8") as f: - f.write(hugo_content) - - print( - f"Processed index: {source_path.relative_to(OBSIDIAN_VAULT)} -> {dest_path.relative_to(HUGO_CONTENT)}" - ) - - -def extract_frontmatter(content): - """Extract YAML frontmatter from markdown content.""" - if content.startswith("---\n"): - parts = content.split("---\n", 2) - if len(parts) >= 3: - try: - frontmatter = yaml.safe_load(parts[1]) - return frontmatter, parts[2] - except yaml.YAMLError: - return {}, content - return {}, content - - -def convert_obsidian_links(content): - """Convert Obsidian-style links to Hugo markdown.""" - # Convert [[Page Name]] to [Page Name](page-name) - content = re.sub( - r"\[\[(.*?)\]\]", - lambda m: f"[{clean_title(m.group(1))}]({clean_title(m.group(1)).lower().replace(' ', '-')})", - content, - ) - - # Convert ![[image.png]] to ![](image.png) - content = re.sub(r"!\[\[(.*?)\]\]", r"![](\1)", content) - - # Convert callouts to Hugo shortcodes - content = re.sub( - r"> \[!(\w+)\](.*?)\n", r"{{< callout \1 >}}\2{{< /callout >}}\n", content - ) - - return content - - -def determine_resource_category(file_path: Path) -> Optional[str]: - """Determine the resource category based on the file path.""" - relative_path = file_path.relative_to(OBSIDIAN_VAULT) - if len(relative_path.parts) < 3: # Must be at least "01 Resources/category/file.md" - return None - - # The category is the second path component under "01 Resources" - category = clean_title(relative_path.parts[1]) - return category.lower() - - -def process_file(file_path: Path): - """Process a single markdown file.""" - logger = logging.getLogger() - - try: - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - - frontmatter, content = extract_frontmatter(content) - - # Skip if not marked for publishing - if not frontmatter.get("publish", False): - logger.debug(f"Skipping unpublished file: {file_path}") - return - - # Convert content - logger.debug(f"Converting content for: {file_path}") - content = convert_obsidian_links(content) - - # Determine the section - section = determine_section(file_path) - logger.debug(f"Determined section '{section}' for: {file_path}") - - # Clean up the title - title = frontmatter.get("title", clean_title(file_path.stem)) - logger.debug(f"Using title: {title}") - - # Prepare Hugo frontmatter - hugo_frontmatter = { - "title": title, - "date": frontmatter.get("date", datetime.now().strftime("%Y-%m-%d")), - "lastmod": datetime.now().strftime("%Y-%m-%d"), - "draft": False, - } - - # Add section-specific frontmatter - if section == "blog": - hugo_frontmatter["type"] = "post" - logger.debug("Setting type: post") - - elif section == "resources": - hugo_frontmatter["type"] = "page" - logger.debug("Setting type: page (resource)") - - # Set the category based on the subdirectory - category = determine_resource_category(file_path) - if category: - hugo_frontmatter["categories"] = [category] - logger.debug(f"Setting resource category: {category}") - - else: - hugo_frontmatter["type"] = "page" - logger.debug("Setting type: page") - - # Copy over other relevant frontmatter fields - for key in ["tags", "description", "aliases", "weight"]: - if key in frontmatter: - hugo_frontmatter[key] = frontmatter[key] - logger.debug(f"Copied frontmatter field: {key}") - - # If frontmatter already has categories and we're not in resources section, - # preserve them - if "categories" in frontmatter and section != "resources": - hugo_frontmatter["categories"] = frontmatter["categories"] - logger.debug("Preserved existing categories") - - # Create Hugo content - hugo_content = f"---\n{yaml.dump(hugo_frontmatter)}---\n\n{content}" - - # Determine destination path - rel_path = file_path.relative_to(OBSIDIAN_VAULT) - clean_rel_path = clean_path(rel_path) - dest_path = HUGO_CONTENT / section / clean_rel_path - - # Create parent directories - dest_path.parent.mkdir(parents=True, exist_ok=True) - - # Write the file - with open(dest_path, "w", encoding="utf-8") as f: - f.write(hugo_content) - - logger.info(f"Processed: {rel_path} -> {dest_path.relative_to(HUGO_CONTENT)}") - - except Exception as e: - logger.error(f"Failed to process file {file_path}: {str(e)}", exc_info=True) - raise - - -def cleanup_content_directory(dir): - shutil.rmtree(dir) - dir.mkdir(parents=True, exist_ok=True) - - -def main(): - logger = setup_logging() - logger.info("Starting Obsidian to Hugo sync") - logger.debug(f"OBSIDIAN_VAULT: {OBSIDIAN_VAULT}") - logger.debug(f"HUGO_CONTENT: {HUGO_CONTENT}") - - try: - # Validate environment - if not OBSIDIAN_VAULT or not HUGO_CONTENT: - raise ValueError("OBSIDIAN_VAULT and HUGO_CONTENT must be set") - - if not OBSIDIAN_VAULT.exists(): - raise ValueError(f"Obsidian vault not found: {OBSIDIAN_VAULT}") - - # Clean and recreate content directory - if HUGO_CONTENT.exists(): - logger.info("Cleaning existing content directory") - shutil.rmtree(HUGO_CONTENT) - HUGO_CONTENT.mkdir(exist_ok=True) - - # Find and process published notes - logger.info("Finding published notes") - published_notes = find_published_notes(OBSIDIAN_VAULT) - logger.info(f"Found {len(published_notes)} published notes") - - # Process each published note - for note in published_notes: - process_file(note) - - logger.info("Sync completed successfully") - - except Exception as e: - logger.error(f"Sync failed: {str(e)}", exc_info=True) - raise - - -if __name__ == "__main__": - main() diff --git a/src/guhcampos/README.md b/src/guhcampos/README.md new file mode 100644 index 0000000..6ffab3a --- /dev/null +++ b/src/guhcampos/README.md @@ -0,0 +1,22 @@ +# guhcampos website builder + +Hello! If you're reading this you're probably interested on the code I use to +generate my website, so you have probably been there. + +I'm opensourcing this as a fun project to showcase, a purposedly overengineered +way to exercise some good practices in development and keep my brain working while +taking a break from work or Factorio (which are kind of the same thing?). + +Please feel free to suggest (potentially useless?) stuff to add here for fun or +(even better so) profit. I'm also happy to contribute any (I doubt it) useful +code to other projects. + +In the unlikely event that someone else besides future-me decides to use this, +the project is installable using `uv`, so all you need to do is: + +``` +uv sync +uv run guhcampos --help +``` + +Everything else should make sense (I doubt it will, even for future me.) diff --git a/src/guhcampos/cli.py b/src/guhcampos/cli.py new file mode 100644 index 0000000..6628019 --- /dev/null +++ b/src/guhcampos/cli.py @@ -0,0 +1,87 @@ +import click +from rich.console import Console + +from .hugo import build_hugo +from .logging import setup_logging +from .obsidian.client import pull_obsidian_content +from .settings import settings +from .spotify.client import ( + fetch_spotify_playlists, + list_spotify_playlists, +) + +setup_logging(logdir=settings.log_dir) + +console = Console() + + +@click.group() +def guhcampos() -> None: + pass + + +@guhcampos.group() +def hugo(): + pass + + +@hugo.command() +def build() -> None: + build_hugo(src=settings.hugo_src_dir, dst=settings.hugo_dst_dir) + + +@guhcampos.group() +def obsidian(): + pass + + +@obsidian.command() +def pull() -> None: + pull_obsidian_content( + posts_path=settings.obsidian_vault_root / "03 Posts", + dst_dir=settings.obsidian_content_output_dir, + ) + + +@guhcampos.group() +def spotify() -> None: + pass + + +@spotify.command() +def fetch_playlists() -> None: + """ + Fetch playlists from Spotify and save them to the output directory as both + M3U and JSON files. + """ + playlists = settings.spotify_playlists + + success = fetch_spotify_playlists( + playlists, + settings.spotify_playlists_output_dir, + settings.spotify_user_id, + settings.spotify_client_id, + settings.spotify_client_secret, + ) + + if not success: + raise click.ClickException("Failed to fetch playlists") + + +@spotify.command() +def list_playlists() -> None: + """ + List all playlists for the configured user. + """ + success = list_spotify_playlists( + settings.spotify_user_id, + settings.spotify_client_id, + settings.spotify_client_secret, + ) + + if not success: + raise click.ClickException("Failed to list playlists") + + +if __name__ == "__main__": + guhcampos() diff --git a/src/guhcampos/core.py b/src/guhcampos/core.py new file mode 100644 index 0000000..6c05758 --- /dev/null +++ b/src/guhcampos/core.py @@ -0,0 +1,135 @@ +import subprocess +from pathlib import Path + +from rich.console import Console + +console = Console() + + +class HugoBuilder: + """Hugo site builder with rich output and error handling.""" + + def __init__(self, hugo_dir: Path, output_dir: Path): + self.hugo_dir = hugo_dir + self.output_dir = output_dir + + def validate_setup(self) -> bool: + """Validate that Hugo is properly set up.""" + if not self.hugo_dir.exists(): + console.print( + f"[red]Error: Hugo directory '{self.hugo_dir}' does not exist[/red]" + ) + return False + + if not (self.hugo_dir / "hugo.toml").exists(): + console.print(f"[red]Error: No hugo.toml found in '{self.hugo_dir}'[/red]") + return False + + return True + + def check_hugo_installed(self) -> bool: + """Check if Hugo is installed and available.""" + try: + result = subprocess.run( + ["hugo", "version"], + capture_output=True, + text=True, + check=True, + ) + console.print(f"[green]Found Hugo: {result.stdout.strip()}[/green]") + return True + except (subprocess.CalledProcessError, FileNotFoundError): + console.print("[red]Error: Hugo is not installed or not in PATH[/red]") + return False + + def build( + self, + draft: bool = False, + future: bool = False, + minify: bool = False, + verbose: bool = False, + ) -> bool: + """Build the Hugo site.""" + if not self.validate_setup(): + return False + + if not self.check_hugo_installed(): + return False + + # Prepare Hugo command + cmd = ["hugo"] + + if not draft: + cmd.append("--buildDrafts=false") + if not future: + cmd.append("--buildFuture=false") + if minify: + cmd.append("--minify") + if verbose: + cmd.append("--verbose") + + cmd.extend( + [ + "--source", + str(self.hugo_dir), + "--destination", + str(self.output_dir), + ] + ) + + # Execute build + try: + result = subprocess.run( + cmd, + cwd=self.hugo_dir, + capture_output=True, + text=True, + check=True, + ) + + if verbose and result.stdout: + console.print("\n[dim]Build output:[/dim]") + console.print(result.stdout) + + return True + + except subprocess.CalledProcessError as e: + console.print( + f"\n[red]Error: Hugo build failed with exit code {e.returncode}[/red]" + ) + if e.stdout: + console.print(f"\n[dim]stdout:[/dim]\n{e.stdout}") + if e.stderr: + console.print(f"\n[dim]stderr:[/dim]\n{e.stderr}") + return False + + def get_build_stats(self) -> dict | None: + """Get statistics about the built site.""" + if not self.output_dir.exists(): + return None + + total_files = sum(1 for _ in self.output_dir.rglob("*") if _.is_file()) + total_size = sum( + f.stat().st_size for f in self.output_dir.rglob("*") if f.is_file() + ) + + return { + "files": total_files, + "size_bytes": total_size, + "size_mb": total_size / 1024 / 1024, + } + + +def get_project_root() -> Path: + """Get the project root directory.""" + return Path(__file__).parent.parent.parent + + +def get_hugo_dir() -> Path: + """Get the Hugo directory path.""" + return get_project_root() / "hugo" + + +def get_output_dir() -> Path: + """Get the default output directory path.""" + return get_hugo_dir() / "public" diff --git a/src/guhcampos/hugo.py b/src/guhcampos/hugo.py new file mode 100644 index 0000000..c0b36d1 --- /dev/null +++ b/src/guhcampos/hugo.py @@ -0,0 +1,47 @@ +import subprocess +import sys +from pathlib import Path + +from rich.console import Console +from rich.panel import Panel + +console = Console() + + +def build_hugo(src: Path, dst: Path): + cmd = [ + "hugo", + "--minify", + "--source", + str(src), + "--destination", + str(dst), + "--cleanDestinationDir", + ] + console.print(f"Command: {' '.join(cmd)}") + console.print( + Panel( + f"[bold]Building Hugo site[/bold]\nCommand: {' '.join(cmd)}", + title="Build Configuration", + border_style="blue", + ) + ) + + try: + subprocess.run( + cmd, + cwd=src, + capture_output=True, + text=True, + check=True, + ) + + except subprocess.CalledProcessError as e: + console.print( + f"\n[red]Error: Hugo build failed with exit code {e.returncode}[/red]" + ) + if e.stdout: + console.print(f"\n[dim]stdout:[/dim]\n{e.stdout}") + if e.stderr: + console.print(f"\n[dim]stderr:[/dim]\n{e.stderr}") + sys.exit(1) diff --git a/src/guhcampos/jdecimal.py b/src/guhcampos/jdecimal.py new file mode 100644 index 0000000..e830cab --- /dev/null +++ b/src/guhcampos/jdecimal.py @@ -0,0 +1,13 @@ +""" +Johnny Decimal related utilities. +""" + +import re + + +def strip_jdecimal_id(s: str) -> str: + """ + Strip a Johnny Decimal ID from a string. Useful for publishing Johnny Decimal + organized notes and content to the website without the organization clutter. + """ + return re.sub(r"^\d+(\.\d+)+\s", "", s) diff --git a/src/guhcampos/letterboxd.py b/src/guhcampos/letterboxd.py new file mode 100644 index 0000000..e69de29 diff --git a/src/guhcampos/logging.py b/src/guhcampos/logging.py new file mode 100644 index 0000000..f69ecc9 --- /dev/null +++ b/src/guhcampos/logging.py @@ -0,0 +1,12 @@ +import sys +from pathlib import Path + +from loguru import logger + + +def setup_logging(logdir: Path): + logger.remove() + + fmt = "{time} - {name} - {level} - {message}" + logger.add(logdir / "debug.log", level="DEBUG", format=fmt) + logger.add(sys.stderr, level="INFO", format=fmt) diff --git a/src/guhcampos/obsidian/client.py b/src/guhcampos/obsidian/client.py new file mode 100644 index 0000000..81958e9 --- /dev/null +++ b/src/guhcampos/obsidian/client.py @@ -0,0 +1,76 @@ +from collections.abc import Generator +from pathlib import Path +from typing import Any + +import yaml +from loguru import logger + +from ..jdecimal import strip_jdecimal_id +from ..utils import ensure_dir +from .errors import ObsidianPostError +from .models import ObsidianPost + + +def pull_obsidian_content( + posts_path: Path, + dst_dir: Path, +) -> None: + published_posts = gen_published_posts(src=posts_path) + + ensure_dir(dst_dir / "en" / "posts") + ensure_dir(dst_dir / "pt-br" / "posts") + + for post in published_posts: + filename = dst_dir / post.language / "posts" / f"{post.slug}.md" + logger.info(f"writing {filename}") + with open(filename, "w") as f: + f.write(post.as_hugo_post()) + + +def gen_published_posts(src: Path) -> Generator[ObsidianPost, Any, Any]: + errors = 0 + + for path in src.rglob("*.md"): + with open(path) as f: + try: + post = parse_obsidian_note(f.read()) + if post.publish: + yield post + + except ObsidianPostError as e: + errors += 1 + logger.warning("Failed to parse an obsidian post") + logger.debug(e) + + logger.info(f"Failed to parse {errors} obsidian posts") + + +def parse_obsidian_note(data: str, series: str | None = None) -> ObsidianPost: + def _parse_tags(tags: list[str]) -> list[str]: + tagset = set() + + for tag in tags: + for t in tag.split("/"): + tagset.add(t) + + return list(tagset) + + try: + _, header, body = data.split("---", 2) + title, content = body.strip("\n").split("\n", 1) + frontmatter = yaml.safe_load(header) + tags = _parse_tags(frontmatter.pop("tags")) + + print(80 * "#") + print(tags) + print(80 * "#") + + return ObsidianPost( + content=content.strip("\n"), + title=strip_jdecimal_id(title.strip("# ").strip("\n")), + tags=tags, + **frontmatter, + ) + except Exception as e: + logger.debug(f"Failed to parse obsidian post: frontmatter: {frontmatter}") + raise ObsidianPostError(f"Failed to parse obsidian post: {e}") from e diff --git a/src/guhcampos/obsidian/errors.py b/src/guhcampos/obsidian/errors.py new file mode 100644 index 0000000..92396fc --- /dev/null +++ b/src/guhcampos/obsidian/errors.py @@ -0,0 +1,2 @@ +class ObsidianPostError(Exception): + pass diff --git a/src/guhcampos/obsidian/models.py b/src/guhcampos/obsidian/models.py new file mode 100644 index 0000000..794fa0c --- /dev/null +++ b/src/guhcampos/obsidian/models.py @@ -0,0 +1,41 @@ +import datetime + +import slugify +import yaml +from pydantic import BaseModel + + +class ObsidianPost(BaseModel): + content: str + language: str + publish_date: datetime.date + publish: bool + tags: list[str] + title: str + series: str | None = None + + @property + def slug(self) -> str: + return slugify.slugify(self.title) + + def as_hugo_post(self) -> str: + return ( + "---\n" + + f"date: {self.publish_date}\n" + + f"title: {self.title}\n" + + f"tags: \n{yaml.dump(self.tags)}\n" + + "---\n" + + self.content + ) + + # return ( + # dedent(f""" + # --- + # + # title: {self.title} + # tags: {yaml.dump(self.tags)} + # --- + # """) + # + self.content + # ) + # # topics: {", ".join(self.topics)} diff --git a/src/guhcampos/settings.py b/src/guhcampos/settings.py new file mode 100644 index 0000000..d82466e --- /dev/null +++ b/src/guhcampos/settings.py @@ -0,0 +1,35 @@ +from pathlib import Path + +from pydantic_settings import BaseSettings, SettingsConfigDict + +REPO_ROOT = Path(__file__).parent.parent.parent + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="GUHCAMPOS_") + + hugo_src_dir: Path = REPO_ROOT / "hugo" + hugo_dst_dir: Path = REPO_ROOT / "public" + + log_dir: Path = REPO_ROOT / "log" + + obsidian_vault_root: Path = REPO_ROOT / "obsidian" + # obsidian_content_output_dir: Path = REPO_ROOT / "hugo" / "build" / "obsidian" + obsidian_content_output_dir: Path = REPO_ROOT / "hugo/content" + obsidian_content_posts_dir: str = "03 Posts" + obsidian_content_mappings: dict[str, str] = { + "03 Posts": "posts", + } + + spotify_client_id: str + spotify_client_secret: str + spotify_user_id: str + spotify_playlists: list[str] = [ + "This is Guhcampos", + "Beard Growing", + "Banda Indie Canta Hey!", + ] + spotify_playlists_output_dir: Path = REPO_ROOT / "hugo" / "static" / "playlists" + + +settings = Settings() # type: ignore diff --git a/src/guhcampos/spotify/client.py b/src/guhcampos/spotify/client.py new file mode 100644 index 0000000..c8b660d --- /dev/null +++ b/src/guhcampos/spotify/client.py @@ -0,0 +1,236 @@ +import base64 +import html +from collections.abc import Generator +from pathlib import Path + +import requests +from rich.console import Console +from rich.panel import Panel +from rich.progress import Progress, SpinnerColumn, TextColumn + +from ..jdecimal import strip_jdecimal_id +from .models import SpotifyPlaylist, SpotifyTrack + +console = Console() + + +class SpotifyAPI: + def __init__(self, client_id: str, client_secret: str): + self.client_id = client_id + self.client_secret = client_secret + self.access_token = None + self.base_url = "https://api.spotify.com/v1" + + def authenticate(self) -> bool: + def _get_basic_auth() -> str: + credentials = f"{self.client_id}:{self.client_secret}" + return base64.b64encode(credentials.encode()).decode() + + auth_url = "https://accounts.spotify.com/api/token" + auth_data = {"grant_type": "client_credentials"} + auth_headers = {"Authorization": f"Basic {_get_basic_auth()}"} + + console.print("[yellow]Authenticating with Spotify...[/yellow]") + + try: + response = requests.post(auth_url, data=auth_data, headers=auth_headers) + response.raise_for_status() + + self.access_token = response.json()["access_token"] + + if not self.access_token: + raise Exception("Authentication failed") + + console.print("[green]Authentication successful![/green]") + return True + + except requests.RequestException as e: + console.print(f"[red]Authentication failed: {e}[/red]") + raise + + def fetch_playlists(self, user_id: str) -> Generator[SpotifyPlaylist, None, None]: + """ + Yield all playlists for a given user. + """ + headers = {"Authorization": f"Bearer {self.access_token}"} + limit = 50 # Maximum allowed by Spotify API on a single paginated request + offset = 0 + + while True: + params = {"limit": limit, "offset": offset} + + try: + response = requests.get( + f"{self.base_url}/users/{user_id}/playlists", + headers=headers, + params=params, + ) + response.raise_for_status() + + data = response.json() + playlists = data.get("items", []) + + if not playlists: + break + + for playlist in playlists: + yield SpotifyPlaylist( + description=html.unescape(playlist["description"]), + owner=playlist["owner"]["display_name"], + url=playlist["external_urls"]["spotify"], + id=playlist["id"], + name=strip_jdecimal_id(playlist["name"]), + tracks=[], # self.get_playlist_tracks(playlist["id"]), + ) + + if len(playlists) < limit: + break + + offset += limit + + except requests.RequestException as e: + console.print(f"[red]Failed to fetch playlists: {e}[/red]") + break + + def get_playlist_tracks(self, playlist_id: str) -> list[SpotifyTrack]: + """ + Get all tracks from a playlist. + """ + + headers = {"Authorization": f"Bearer {self.access_token}"} + tracks = [] + url = f"{self.base_url}/playlists/{playlist_id}/tracks" + + while url: + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + + data = response.json() + tracks.extend(data.get("items", [])) + + url = data.get("next") + + except requests.RequestException as e: + console.print(f"[red]Failed to fetch tracks: {e}[/red]") + break + + tracks.sort(key=lambda x: x["track"]["name"]) + tracks.sort(key=lambda x: x["track"]["artists"][0]["name"]) + + return [ + SpotifyTrack( + name=track["track"]["name"], + artists=[artist["name"] for artist in track["track"]["artists"]], + album=track["track"]["album"]["name"], + duration_ms=track["track"]["duration_ms"], + url=track["track"]["external_urls"]["spotify"], + ) + for track in tracks + ] + + +def fetch_spotify_playlists( + playlists_filter: list[str], + output_dir: Path, + user_id: str, + client_id: str, + client_secret: str, +): + """ + Fetch all Spotify playlists for a user and save them as M3U files. + + Args: + user_id: Spotify user ID + output_dir: Directory to save M3U files + client_id: Spotify client ID + client_secret: Spotify client secret + + Returns: + True if successful, False otherwise + """ + spotify = SpotifyAPI(client_id, client_secret) + spotify.authenticate() + + output_dir.mkdir(parents=True, exist_ok=True) + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + for playlist in spotify.fetch_playlists(user_id): + if playlist.name in playlists_filter: + task = progress.add_task(f"Processing '{playlist.name}'...", total=None) + + playlist.tracks = spotify.get_playlist_tracks(playlist.id) + + progress.update( + task, + description=f"[green]Processed '{playlist.name}' ({len(playlist.tracks)} tracks)[/green]", + ) + + with open( + output_dir / f"{playlist.slug}.m3u", "w", encoding="utf-8" + ) as f: + f.write(playlist.to_m3u()) + + with open( + output_dir / f"{playlist.slug}.json", "w", encoding="utf-8" + ) as f: + f.write(playlist.to_json()) + + return True + + +def list_spotify_playlists( + user_id: str, + client_id: str, + client_secret: str, +): + """ + List all playlists for a user using pagination. + + Args: + user_id: Spotify user ID + client_id: Spotify client ID + client_secret: Spotify client secret + + Returns: + True if successful, False otherwise + """ + + spotify = SpotifyAPI(client_id, client_secret) + spotify.authenticate() + + playlists = [] + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + task = progress.add_task("Fetching playlists...", total=None) + + try: + for playlist in spotify.fetch_playlists(user_id): + playlists.append(playlist) + progress.update( + task, description=f"Found {len(playlists)} playlists..." + ) + + except Exception as e: + progress.update(task, description=f"[red]Error: {e}[/red]") + return False + console.print( + Panel( + f"[bold green]Found {len(playlists)} playlists[/bold green]\n\n" + + "\n".join( + [f"• {p.name} ({len(p.tracks)} tracks) - {p.url})" for p in playlists] + ), + title="📋 All Playlists", + border_style="blue", + ) + ) + + return True diff --git a/src/guhcampos/spotify/models.py b/src/guhcampos/spotify/models.py new file mode 100644 index 0000000..413db19 --- /dev/null +++ b/src/guhcampos/spotify/models.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel, HttpUrl +from slugify import slugify + + +class SpotifyTrack(BaseModel): + name: str + artists: list[str] + album: str + duration_ms: int + url: HttpUrl + + @property + def duration(self) -> int: + return self.duration_ms // 1000 + + +class SpotifyPlaylist(BaseModel): + id: str + name: str + tracks: list[SpotifyTrack] + url: HttpUrl + description: str + owner: str + + @property + def slug(self) -> str: + return slugify(self.name) + + def to_m3u(self) -> str: + output = [ + "#EXTM3U", + f"# {self.name}", + f"# Description:{self.description}", + f"# Created by: {self.owner}", + f"# Tracks: {len(self.tracks)}", + f"# Spotify URL: {self.url}", + ] + + for track in self.tracks: + artists_str = ", ".join(track.artists) + output.append( + f"#EXTINF:{track.duration},{artists_str} - {track.album} - {track.name}" + ) + output.append(track.url.encoded_string()) + return "\n".join(output) + + def to_json(self) -> str: + return self.model_dump_json() diff --git a/src/guhcampos/utils.py b/src/guhcampos/utils.py new file mode 100644 index 0000000..ec14f69 --- /dev/null +++ b/src/guhcampos/utils.py @@ -0,0 +1,5 @@ +from pathlib import Path + + +def ensure_dir(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) diff --git a/themes/blowfish b/themes/blowfish deleted file mode 160000 index 0b06a64..0000000 --- a/themes/blowfish +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0b06a64139beba6287e7685f4c810ad4ff772fde diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0906d1b --- /dev/null +++ b/uv.lock @@ -0,0 +1,613 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, + { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, + { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" }, + { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" }, + { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" }, +] + +[[package]] +name = "guhcampos" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "loguru" }, + { name = "pathspec" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "watchdog" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "click", specifier = ">=8.1.0" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, + { name = "loguru", specifier = ">=0.7.3" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "pathspec", specifier = ">=0.11.0" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "pydantic-settings", specifier = ">=2.10.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "python-slugify", specifier = ">=8.0.4" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "watchdog", specifier = ">=3.0.0" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.4.1" }] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" }, + { url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" }, + { url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" }, + { url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" }, + { url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" }, + { url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +]