From 839f67ad0a86f848b5578b111af9095724a27b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?c=C4=83t=C4=83lin?= Date: Wed, 5 Mar 2025 12:40:19 +0100 Subject: [PATCH] feat: add UpdateVersionAction --- .gitignore | 1 - charts/huesoporro/Chart.yaml | 22 +- charts/huesoporro/values.yaml | 172 +++++---------- pyproject.toml | 1 + src/apps/cli/typer/main.py | 6 + src/huesoporro/actions/misc/__init__.py | 0 .../actions/misc/update_version_action.py | 198 ++++++++++++++++++ uv.lock | 11 + 8 files changed, 266 insertions(+), 145 deletions(-) create mode 100644 src/huesoporro/actions/misc/__init__.py create mode 100644 src/huesoporro/actions/misc/update_version_action.py diff --git a/.gitignore b/.gitignore index 8ad2561..d9c858c 100644 --- a/.gitignore +++ b/.gitignore @@ -88,7 +88,6 @@ celerybeat-schedule celerybeat.pid *.sage.py .env -.venv env/ venv/ ENV/ diff --git a/charts/huesoporro/Chart.yaml b/charts/huesoporro/Chart.yaml index 716c06d..9c41582 100644 --- a/charts/huesoporro/Chart.yaml +++ b/charts/huesoporro/Chart.yaml @@ -1,24 +1,6 @@ apiVersion: v2 -name: huesoporro +appVersion: 0.3.0 description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. +name: huesoporro type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) version: 0.3.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "0.3.0" diff --git a/charts/huesoporro/values.yaml b/charts/huesoporro/values.yaml index 54c4d4d..0543d63 100644 --- a/charts/huesoporro/values.yaml +++ b/charts/huesoporro/values.yaml @@ -1,138 +1,62 @@ -# Default values for helm. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ -replicaCount: 1 - -# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ -image: - repository: git.roboces.dev/catalin/huesoporro - # This sets the pull policy for images. - pullPolicy: Always - # Overrides the image tag whose default is the chart appVersion. - tag: "0.3.0" - -# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ -imagePullSecrets: [] -# This is to override the chart name. -nameOverride: "" -fullnameOverride: "" - -#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ -serviceAccount: - # Specifies whether a service account should be created - create: true - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -# This is for setting Kubernetes Annotations to a Pod. -# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ -podAnnotations: {} -# This is for setting Kubernetes Labels to a Pod. -# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ -service: - # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types - type: LoadBalancer - # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports - port: 8000 - -# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ -ingress: +affinity: {} +autoscaling: enabled: false - className: "" + maxReplicas: 100 + minReplicas: 1 + targetCPUUtilizationPercentage: 80 +fullnameOverride: '' +image: + pullPolicy: Always + repository: git.roboces.dev/catalin/huesoporro + tag: 0.3.0 +imagePullSecrets: [] +ingress: annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" + className: '' + enabled: false hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ livenessProbe: httpGet: path: /healthz port: http +nameOverride: '' +nodeSelector: {} +persistence: + accessModes: + - ReadWriteMany + annotations: {} + enabled: false + size: 10Gi + storageClassName: default + volumeOwner: + enabled: true + gid: 1000 + uid: 1000 +podAnnotations: {} +podLabels: {} +podSecurityContext: {} readinessProbe: httpGet: path: /healthz port: http - -#This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# Additional volumes on the output Deployment definition. -volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -persistence: - enabled: false - accessModes: - - ReadWriteMany - size: 10Gi - storageClassName: "default" - volumeOwner: - enabled: true - uid: 1000 - gid: 1000 - annotations: {} - +replicaCount: 1 +resources: {} secret: existingSecretName: huesoporro-secrets +securityContext: {} +service: + port: 8000 + type: LoadBalancer +serviceAccount: + annotations: {} + automount: true + create: true + name: '' +tolerations: [] +volumeMounts: [] +volumes: [] diff --git a/pyproject.toml b/pyproject.toml index c30ecf0..9a2888f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dev-dependencies = [ "ruff>=0.8.3", "pytest-coverage>=0.0", "polyfactory>=2.18.1", + "types-pyyaml>=6.0.12.20241230", ] [[tool.mypy.overrides]] diff --git a/src/apps/cli/typer/main.py b/src/apps/cli/typer/main.py index 5f14c84..e69f8b3 100644 --- a/src/apps/cli/typer/main.py +++ b/src/apps/cli/typer/main.py @@ -4,6 +4,7 @@ from loguru import logger from typer import Typer from huesoporro.actions.import_from_vod import ImportFromVODAction +from huesoporro.actions.misc.update_version_action import UpdateVersionAction from huesoporro.settings import Settings from huesoporro.svc.clean_cc_svc import CleanCCSvc from huesoporro.svc.download_closed_captions import DownloadClosedCaptionsSvc @@ -22,3 +23,8 @@ def import_vod_cc(channel_name: str, youtube_url: str, db_path: Path | None = No ) for cc_filepath in import_from_vod_action.run(channel_name, youtube_url): logger.info(f"Closed captions imported from {cc_filepath}") + + +@app.command() +def update_version(version: str, dry_run: bool = False): + UpdateVersionAction().run(version, dry_run) diff --git a/src/huesoporro/actions/misc/__init__.py b/src/huesoporro/actions/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/huesoporro/actions/misc/update_version_action.py b/src/huesoporro/actions/misc/update_version_action.py new file mode 100644 index 0000000..cd2206c --- /dev/null +++ b/src/huesoporro/actions/misc/update_version_action.py @@ -0,0 +1,198 @@ +import re +from collections.abc import Callable +from difflib import unified_diff +from pathlib import Path + +import yaml +from loguru import logger +from pydantic import BaseModel, ConfigDict +from rich import print # noqa: A004 +from rich.console import Console +from rich.panel import Panel +from rich.syntax import Syntax + + +class UpdateVersionAction(BaseModel): + project_root: Path = Path(__file__).parents[4] + files_to_update: dict[str, Callable] + console: Console = Console() + + model_config = ConfigDict(arbitrary_types_allowed=True) + + def __init__(self, **data): + files_to_update = { + "pyproject.toml": self._update_pyproject_toml, + "charts/huesoporro/values.yaml": self._update_values_yaml, + "charts/huesoporro/Chart.yaml": self._update_chart_yaml, + } + super().__init__(**data, files_to_update=files_to_update) + + def _read_file(self, filepath: Path) -> str: + """ + Read the contents of a file. + + Args: + filepath (Path): Path to the file to read. + + Returns: + str: File contents + """ + with filepath.open("r") as f: + return f.read() + + def _write_file(self, filepath: Path, content: str): + """ + Write content to a file. + + Args: + filepath (Path): Path to the file to write. + content (str): Content to write to the file. + """ + with filepath.open("w") as f: + f.write(content) + + def _update_pyproject_toml(self, filepath: Path, new_version: str) -> str: + """ + Update version in pyproject.toml. + + Args: + filepath (Path): Path to pyproject.toml + new_version (str): New version to set + + Returns: + str: Updated file content + """ + content = self._read_file(filepath) + version_pattern = r'(version\s*=\s*)[\'"](.+?)[\'"]' + return re.sub(version_pattern, rf'\1"{new_version}"', content) + + def _update_values_yaml(self, filepath: Path, new_version: str) -> str: + """ + Update image tag in values.yaml. + + Args: + filepath (Path): Path to values.yaml + new_version (str): New version to set + + Returns: + str: Updated file content + """ + with filepath.open("r") as file: + values = yaml.safe_load(file) + + # Assumes image.tag exists in the values.yaml + values["image"]["tag"] = new_version + + return yaml.dump(values, default_flow_style=False) + + def _update_chart_yaml(self, filepath: Path, new_version: str) -> str: + """ + Update version and appVersion in Chart.yaml. + + Args: + filepath (Path): Path to Chart.yaml + new_version (str): New version to set + + Returns: + str: Updated file content + """ + with filepath.open("r") as file: + chart_data = yaml.safe_load(file) + + chart_data["version"] = new_version + chart_data["appVersion"] = new_version + + return yaml.dump(chart_data, default_flow_style=False) + + def _generate_diff(self, original: str, updated: str, filename: str) -> str: + """ + Generate a unified diff between original and updated content. + + Args: + original (str): Original file content + updated (str): Updated file content + filename (str): Name of the file + + Returns: + str: Unified diff representation + """ + # Split content into lines + original_lines = original.splitlines(keepends=True) + updated_lines = updated.splitlines(keepends=True) + + # Generate unified diff + diff_lines = list( + unified_diff( + original_lines, + updated_lines, + fromfile=f"a/{filename}", + tofile=f"b/{filename}", + lineterm="", + ) + ) + + return "\n".join(diff_lines) + + def _rich_display_diff(self, diff: str): + """ + Display diff using rich for colorful output. + + Args: + diff (str): Unified diff to display + """ + if not diff: + return + + # Use Syntax for syntax highlighting + syntax = Syntax(diff, "diff", theme="ansi_dark") + + # Create a panel with the diff + panel = Panel( + syntax, title="Version Update Diff", border_style="cyan", expand=False + ) + + # Display the panel + self.console.print(panel) + + def run(self, new_version: str, dry_run: bool = False): + """ + Update version across all specified files. + + Args: + new_version (str): New version to set + dry_run (bool): Dry run mode with diff display + """ + for relative_path, update_func in self.files_to_update.items(): + filepath = self.project_root / relative_path + + if not filepath.exists(): + logger.warning(f"Warning: {filepath} not found. Skipping.") + continue + + try: + # Read original content + original_content = self._read_file(filepath) + + # Generate updated content + updated_content = update_func(filepath, new_version) + + if dry_run: + # Generate and display diff + diff = self._generate_diff( + original_content, updated_content, str(relative_path) + ) + + # Display the diff + if diff: + print(f"\nDiff for {relative_path}:") + self._rich_display_diff(diff) + else: + # Write updated content + self._write_file(filepath, updated_content) + print(f"Updated {relative_path}") + + except Exception as exc: # noqa: BLE001 + logger.error(f"Error updating {relative_path}: {exc}") + + if dry_run: + print("\nDry run complete. No files were modified.") diff --git a/uv.lock b/uv.lock index f7e072d..f4b6cb4 100644 --- a/uv.lock +++ b/uv.lock @@ -549,6 +549,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-coverage" }, { name = "ruff" }, + { name = "types-pyyaml" }, ] [package.metadata] @@ -582,6 +583,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "pytest-coverage", specifier = ">=0.0" }, { name = "ruff", specifier = ">=0.8.3" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20241230" }, ] [[package]] @@ -1482,6 +1484,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20241230" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029 }, +] + [[package]] name = "typing-extensions" version = "4.12.2"