diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml new file mode 100644 index 0000000..653943c --- /dev/null +++ b/.github/workflows/ci-cd.yaml @@ -0,0 +1,37 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - '**' + tags: + - 'v*.*.*' + pull_request: + branches: + - '**' + workflow_dispatch: + +jobs: + lint: + uses: ./.github/workflows/ruff.yaml + + test: + needs: lint + uses: ./.github/workflows/pytest.yaml + + docker: + needs: test + uses: ./.github/workflows/docker-build-and-scan.yaml + with: + DOCKER_PATH_CONTEXT: . + DOCKER_BUILD_DOCKERFILE: ./Dockerfile + DOCKER_TAGS: ${{ github.repository }}:${{ github.sha }} + DOCKER_LOAD_BOOL: false + DOCKER_PUSH_BOOL: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + secrets: inherit + + release: + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + needs: [test, docker] + uses: ./.github/workflows/release.yaml + secrets: inherit diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index ca1234f..0072a5a 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -1,9 +1,7 @@ -name: Unit Test & Coverage +name: Unit Test & Coverage (Reusable) on: - pull_request: - branches: - - '*' + workflow_call: jobs: unit-test-coverage: @@ -11,7 +9,7 @@ jobs: permissions: contents: read env: - UV_VERSION: '0.10.2' + UV_VERSION: '>=0.10.2' PYTHON_VERSION: '3.13' steps: - name: Checkout repository @@ -45,7 +43,7 @@ jobs: uv run -- coverage report --show-missing - name: Save uv caches - if: steps.cache-restore.outputs.cache-hit != 'true' + if: always() uses: actions/cache/save@v5 with: path: | diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6f576a2..5cc48a7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,10 +1,6 @@ -name: UV - Release - +name: UV - Release (Reusable) on: - push: - branches: - - main - + workflow_call: jobs: Semantic-Release: runs-on: ubuntu-latest @@ -19,7 +15,7 @@ jobs: dist_artifacts_name: dist dist_artifacts_dir: dist lock_file_artifact: uv.lock - UV_VERSION: '0.10.2' + UV_VERSION: '>=0.10.2' PYTHON_VERSION: '3.13' GITHUB_ACTIONS_AUTHOR_NAME: github-actions GITHUB_ACTIONS_AUTHOR_EMAIL: actions@users.noreply.github.com @@ -28,11 +24,9 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.ref_name }} - - name: Setup | Force release branch to be at workflow sha run: | git reset --hard ${{ github.sha }} - - name: Restore global uv cache id: cache-restore uses: actions/cache/restore@v5 @@ -44,31 +38,25 @@ jobs: key: uv-main-${{ env.UV_VERSION }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'uv.lock') }} restore-keys: | uv-main- - - name: Install uv uses: astral-sh/setup-uv@v7 with: version: ${{ env.UV_VERSION }} python-version: ${{ env.PYTHON_VERSION }} enable-cache: false - - name: Action | Semantic Version Release id: release uses: python-semantic-release/python-semantic-release@v10.5.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - git_committer_name: 'github-actions' - git_committer_email: 'actions@users.noreply.github.com' - - - name: Publish | Upload to GitHub Release Assets + - name: Action | Create GitHub Release uses: python-semantic-release/publish-action@v10.5.3 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.release.outputs.tag }} - - name: Save uv caches - if: steps.cache-restore.outputs.cache-hit != 'true' + if: always() uses: actions/cache/save@v5 with: path: | diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml index 0bbc173..3bba328 100644 --- a/.github/workflows/ruff.yaml +++ b/.github/workflows/ruff.yaml @@ -1,9 +1,7 @@ -name: UV Build - Ruff Lint +name: Ruff Lint (Reusable) on: - pull_request: - branches: - - '*' + workflow_call: jobs: ruff-lint: @@ -11,7 +9,7 @@ jobs: permissions: contents: read env: - UV_VERSION: '0.10.2' + UV_VERSION: '>=0.10.2' PYTHON_VERSION: '3.13' steps: @@ -46,7 +44,7 @@ jobs: uv run ruff check tests - name: Save uv caches - if: steps.cache-restore.outputs.cache-hit != 'true' + if: always() uses: actions/cache/save@v5 with: path: | diff --git a/pyproject.toml b/pyproject.toml index 4661ad9..195808a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ build = ["uv >= 0.10.2"] [build-system] -requires = ["uv_build >= 0.10.2, < 0.11.0"] +requires = ["uv_build >= 0.10.2"] build-backend = "uv_build" [project.scripts] diff --git a/src/sample_python_app/core/display.py b/src/sample_python_app/core/display.py index 9e94e4d..f7d26d7 100644 --- a/src/sample_python_app/core/display.py +++ b/src/sample_python_app/core/display.py @@ -1,7 +1,13 @@ """Handles formatting and displaying astronomical data using rich and pyfiglet.""" +from datetime import datetime + from pyfiglet import Figlet +from rich.align import Align from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text from sample_python_app.core.config import settings from sample_python_app.core.logging import setup_logger @@ -19,16 +25,107 @@ def display_astronomical_data(astro): """ logger = setup_logger(mode="silent") console = Console() - header = Figlet(font="small", width=100).renderText("Astronomical Data") - logger.info("Displaying Astronomical Data header.") - console.print(f"[bold magenta]{header}[/bold magenta]") + # Synthwave color palette (no longer used) + + header = Figlet(font="slant", width=120).renderText("SYNTHWAVE SUNRISE 🌅") + logger.info("Displaying Synthwave Sunrise header.") + header_text = Text(header) + header_text.stylize("bold magenta") + sunrise_local = astro.sunrise.astimezone(settings.tz) + sunset_local = astro.sunset.astimezone(settings.tz) date_art = Figlet(font="mini", width=150).renderText( sunrise_local.strftime("%A, %B %d, %Y") ) - logger.info(f'Displaying date: {sunrise_local.strftime("%A, %B %d, %Y")}') - console.print(f"[bold cyan]{date_art}[/bold cyan]") - for name, value in astro.formatted(settings.tz, settings.DATE_FORMAT).items(): + logger.info(f'Displaying date: {sunrise_local.strftime("%A, %B %d, %Y")})') + date_text = Text(date_art) + date_text.stylize("bold cyan") + + # Stylized sunrise/sunset + sun_art = Figlet(font="starwars", width=120).renderText("SUNRISE") + sun_set_art = Figlet(font="starwars", width=120).renderText("SUNSET") + sun_text = Text(sun_art) + sun_text.stylize("bold yellow") + # Sunrise time as figlet + sunrise_time_art = Figlet(font="big", width=100).renderText( + sunrise_local.strftime("%H:%M:%S") + ) + sunrise_time_text = Text(sunrise_time_art) + sunrise_time_text.stylize("bold yellow") + + sun_set_text = Text(sun_set_art) + sun_set_text.stylize("bold blue") + # Sunrise time as figlet with AM/PM + sunrise_time_str = sunrise_local.strftime("%I:%M:%S %p") + sunrise_time_art = Figlet(font="big", width=100).renderText(sunrise_time_str) + sunrise_time_text = Text(sunrise_time_art) + sunrise_time_text.stylize("bold yellow") + + sun_set_text = Text(sun_set_art) + sun_set_text.stylize("bold blue") + # Sunset time as figlet with AM/PM + sunset_time_str = sunset_local.strftime("%I:%M:%S %p") + sunset_time_art = Figlet(font="big", width=100).renderText(sunset_time_str) + sunset_time_text = Text(sunset_time_art) + sunset_time_text.stylize("bold blue") + from rich.table import Table + + astro_table = Table(show_header=True, header_style="bold magenta", box=None) + astro_table.add_column("Event", style="bold #ff00cc") + astro_table.add_column("Local Time", style="bold #00eaff") + tz = settings.tz + time_fmt = "%I:%M:%S %p %Z" + # Color mapping for event types + event_colors = { + "sunrise": "#ffe066", # yellow + "sunset": "#5dade2", # blue + "transit": "#ffb347", # orange + "civil twilight begin": "#f7cac9", # pink + "civil twilight end": "#92a8d1", # light blue + "nautical twilight begin": "#f9d423", # gold + "nautical twilight end": "#6a89cc", # purple-blue + "astronomical twilight begin": "#b388ff", # violet + "astronomical twilight end": "#2e86c1", # deep blue + } + for name, dt in astro.as_local(tz).items(): label = name.replace("_", " ").title() + if isinstance(dt, datetime): + value = dt.strftime(time_fmt) + else: + value = str(dt) logger.info(f"Displaying {label}: {value}") - console.print(f"[bold cyan]{label}: [white]{value}[/white]") + # Pick color based on event type + color = event_colors.get(label.lower(), "#e17055") # fallback: coral + astro_table.add_row( + f"[{color}]{label}[/{color}]", f"[{color}]{value}[/{color}]" + ) + + # Compose all parts into a single renderable for the panel + from rich.console import Group + + from rich.columns import Columns + + # Combine sunrise and sunset figlet art and times in the same row + sun_figlet_row = Columns( + [ + Group(Align.center(sun_text), Align.center(sunrise_time_text)), + Group(Align.center(sun_set_text), Align.center(sunset_time_text)), + ], + align="center", + expand=True, + ) + + panel_content = Group( + Align.center(header_text), + Align.center(date_text), + sun_figlet_row, + Align.center(astro_table), + ) + console.print( + Panel( + panel_content, + title="[bold #ff6ec7]Synthwave Astronomical Events[/bold #ff6ec7]", + border_style="#ff00cc", + padding=(1, 2), + ) + ) diff --git a/uv.lock b/uv.lock index 80d09a0..4264de6 100644 --- a/uv.lock +++ b/uv.lock @@ -312,11 +312,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/9b/20c8288dc591129bf9dd7be2c91aec6ef23e450605c3403716bd6c74833e/platformdirs-4.8.0.tar.gz", hash = "sha256:c1d4a51ab04087041dd602707fbe7ee8b62b64e590f30e336e5c99c2d0c542d2", size = 27607, upload-time = "2026-02-14T01:52:03.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/227a7d1b8d80ae55c4b47f271c0870dd7a153aa65353bf71921265df2300/platformdirs-4.8.0-py3-none-any.whl", hash = "sha256:1c1328b4d2ea997bbcb904175a9bde14e824a3fa79f751ea3888d63d7d727557", size = 20647, upload-time = "2026-02-14T01:52:01.915Z" }, ] [[package]]