feat: add UpdateVersionAction

This commit is contained in:
cătălin 2025-03-05 12:40:19 +01:00
commit 839f67ad0a
No known key found for this signature in database
8 changed files with 260 additions and 139 deletions

1
.gitignore vendored
View file

@ -88,7 +88,6 @@ celerybeat-schedule
celerybeat.pid
*.sage.py
.env
.venv
env/
venv/
ENV/

View file

@ -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"

View file

@ -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
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: []

View file

@ -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]]

View file

@ -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)

View file

View file

@ -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.")

11
uv.lock generated
View file

@ -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"