Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions altb/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
import yaml
import typer
import pydantic
from pydantic import BaseModel, BaseSettings, PrivateAttr, StrictBool, StrictStr, Field, BaseConfig
from pydantic import BaseModel, BaseSettings, PrivateAttr, StrictStr, Field, BaseConfig
from rich.console import ConsoleOptions, RenderResult, Console, ConsoleRenderable

from altb.constants import CONFIG_FILE, TYPE_TO_COLOR, PACKAGE_NAME, VERSIONS_DIRECTORY
from altb.common import error_console
from altb.migrator import migrator
from altb.migrator.versions import ConfigVersion


class RichText(ConsoleRenderable):
Expand Down Expand Up @@ -58,6 +60,7 @@ def __rich_console__(
class TagKind(str, Enum):
link = "link"
command = "command"
build = "build"


class BaseTag(BaseModel, abc.ABC):
Expand All @@ -77,7 +80,6 @@ def __eq__(self, other):

class LinkTagSpec(BaseModel):
path: pathlib.Path
is_copy: Optional[StrictBool] = False

class Config(BaseConfig):
extra = pydantic.Extra.forbid
Expand Down Expand Up @@ -114,6 +116,28 @@ class CommandTag(BaseTag):
spec: CommandTagSpec


class HashOptions(BaseModel):
sources_directories: List[str]
match_glob: List[str]
ignore_glob: List[str]


class BuildTagSpec(BaseModel):
build_command: str
working_directory: Optional[pathlib.Path]
env: Optional[dict[StrictStr, StrictStr]]
output_binary_path: str
hash_options: Optional[HashOptions] = None

class Config(BaseConfig):
extra = pydantic.Extra.forbid


class BuildTag(BaseTag):
kind: Literal[TagKind.build] = TagKind.build
spec: BuildTagSpec


TagConfig = Annotated[Union[CommandTag, LinkTag], Field(discriminator="kind")]

TagDict = Dict[str, TagConfig]
Expand Down Expand Up @@ -220,12 +244,14 @@ def assert_valid(self):


class AppConfig(BaseModel):
version: str = ConfigVersion.latest
binaries: Dict[str, BinaryStruct] = {}

class Config(BaseConfig):
json_encoders = {
pathlib.Path: str,
set: list,
ConfigVersion: str,
}
extra = pydantic.Extra.forbid

Expand Down Expand Up @@ -275,7 +301,7 @@ def track_path(self, app_name: str, app_path: pathlib.Path, tag: str = None,

self.remove(app_name, existing_tag)

spec = LinkTagSpec(path=app_path, is_copy=should_copy)
spec = LinkTagSpec(path=app_path)
self.binaries[app_name][tag] = LinkTag(description=description, spec=spec)

def rename_tag(self, app_name: str, tag: str, new_tag: str):
Expand Down Expand Up @@ -346,13 +372,30 @@ class Config(BaseSettings.Config):

_config: AppConfig = PrivateAttr(None)

def config_version(self):
if not self.config_path.exists():
return DEFAULT_CONFIG.copy(), False

with self.config_path.open() as f:
content = yaml.safe_load(f)
return content.get('version', ConfigVersion.latest)

def migrate(self):
with self.config_path.open() as f:
content = yaml.safe_load(f)

new_content = migrator.migrate(content)
with self.config_path.open(mode='w') as f:
yaml.safe_dump(new_content, f)

def save(self):
directory = self.config_path.parent
if not self.config_path.exists():
os.makedirs(directory, exist_ok=True)

to_dump = yaml.safe_load(self._config.json(exclude_none=True))
with self.config_path.open(mode='w') as f:
yaml.safe_dump(yaml.safe_load(self._config.json(exclude_none=True)), f)
yaml.safe_dump(to_dump, f)

def load_or_create(self) -> Tuple[AppConfig, bool]:
if not self.config_path.exists():
Expand Down
7 changes: 6 additions & 1 deletion altb/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import typer
import yaml
import pkg_resources
from rich.live import Live
from rich.tree import Tree
from rich.text import Text
Expand Down Expand Up @@ -360,6 +359,12 @@ def schema(
background_color="default", word_wrap=True))


@app.command()
def migrate(ctx: typer.Context):
settings = ctx.ensure_object(Settings)
settings.migrate()


@app.callback(invoke_without_command=True)
def _main(
version: Optional[bool] = typer.Option(None, '-v', '--version', help='Show version', is_eager=True),
Expand Down
Empty file added altb/migrator/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions altb/migrator/definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pathlib

DIRECTORY = pathlib.Path(__file__).absolute().parent
Empty file.
19 changes: 19 additions & 0 deletions altb/migrator/migrations/base_migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import abc
import pathlib
from abc import ABC


MIGRATIONS_DIRECTORY = pathlib.Path(__file__).absolute().parent
SCHEMAS_DIRECTORY = MIGRATIONS_DIRECTORY / 'schemas'


class BaseMigration(ABC):
EXPECTED_SCHEMA: pathlib.Path

@abc.abstractmethod
def migrate(self, input_config: dict) -> dict:
return input_config

@abc.abstractmethod
def rollback(self, input_config: dict) -> dict:
return input_config
11 changes: 11 additions & 0 deletions altb/migrator/migrations/migration_001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .base_migration import BaseMigration, SCHEMAS_DIRECTORY


class MigrationInitial(BaseMigration):
EXPECTED_SCHEMA = SCHEMAS_DIRECTORY / "001_AppConfig.json"

def migrate(self, input_config: dict) -> dict:
return input_config

def rollback(self, input_config: dict) -> dict:
return input_config
16 changes: 16 additions & 0 deletions altb/migrator/migrations/migration_002_remove_is_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .base_migration import BaseMigration, SCHEMAS_DIRECTORY


class MigrationRemoveIsCopy(BaseMigration):
EXPECTED_SCHEMA = SCHEMAS_DIRECTORY / "002_AppConfig.json"

def migrate(self, input_config: dict) -> dict:
for binary in input_config['binaries'].values():
for tag in binary['tags'].values():
if tag['kind'] == 'link':
del tag['spec']['is_copy']

return input_config

def rollback(self, input_config: dict) -> dict:
return input_config
1 change: 1 addition & 0 deletions altb/migrator/migrations/schemas/001_AppConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"title": "AppConfig", "type": "object", "properties": {"version": {"title": "Version", "default": "0.1.0", "type": "string"}, "binaries": {"title": "Binaries", "default": {}, "type": "object", "additionalProperties": {"$ref": "#/definitions/BinaryStruct"}}}, "additionalProperties": false, "definitions": {"CommandTagSpec": {"title": "CommandTagSpec", "type": "object", "properties": {"command": {"title": "Command", "type": "string"}, "working_directory": {"title": "Working Directory", "type": "string", "format": "path"}, "env": {"title": "Env", "type": "object", "additionalProperties": {"type": "string"}}}, "required": ["command"], "additionalProperties": false}, "CommandTag": {"title": "CommandTag", "type": "object", "properties": {"kind": {"title": "Kind", "default": "command", "enum": ["command"], "type": "string"}, "description": {"title": "Description", "type": "string"}, "spec": {"$ref": "#/definitions/CommandTagSpec"}}, "required": ["spec"], "additionalProperties": false}, "LinkTagSpec": {"title": "LinkTagSpec", "type": "object", "properties": {"path": {"title": "Path", "type": "string", "format": "path"}, "is_copy": {"title": "Is Copy", "default": false, "type": "boolean"}}, "required": ["path"], "additionalProperties": false}, "LinkTag": {"title": "LinkTag", "type": "object", "properties": {"kind": {"title": "Kind", "default": "link", "enum": ["link"], "type": "string"}, "description": {"title": "Description", "type": "string"}, "spec": {"$ref": "#/definitions/LinkTagSpec"}}, "required": ["spec"], "additionalProperties": false}, "BinaryStruct": {"title": "BinaryStruct", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "tags": {"title": "Tags", "default": {}, "type": "object", "additionalProperties": {"discriminator": {"propertyName": "kind", "mapping": {"command": "#/definitions/CommandTag", "link": "#/definitions/LinkTag"}}, "anyOf": [{"$ref": "#/definitions/CommandTag"}, {"$ref": "#/definitions/LinkTag"}]}}, "selected": {"title": "Selected", "type": "string"}}, "required": ["name"], "additionalProperties": false}}}
1 change: 1 addition & 0 deletions altb/migrator/migrations/schemas/002_AppConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"title": "AppConfig", "type": "object", "properties": {"version": {"title": "Version", "default": "0.1.0", "type": "string"}, "binaries": {"title": "Binaries", "default": {}, "type": "object", "additionalProperties": {"$ref": "#/definitions/BinaryStruct"}}}, "additionalProperties": false, "definitions": {"CommandTagSpec": {"title": "CommandTagSpec", "type": "object", "properties": {"command": {"title": "Command", "type": "string"}, "working_directory": {"title": "Working Directory", "type": "string", "format": "path"}, "env": {"title": "Env", "type": "object", "additionalProperties": {"type": "string"}}}, "required": ["command"], "additionalProperties": false}, "CommandTag": {"title": "CommandTag", "type": "object", "properties": {"kind": {"title": "Kind", "default": "command", "enum": ["command"], "type": "string"}, "description": {"title": "Description", "type": "string"}, "spec": {"$ref": "#/definitions/CommandTagSpec"}}, "required": ["spec"], "additionalProperties": false}, "LinkTagSpec": {"title": "LinkTagSpec", "type": "object", "properties": {"path": {"title": "Path", "type": "string", "format": "path"}}, "required": ["path"], "additionalProperties": false}, "LinkTag": {"title": "LinkTag", "type": "object", "properties": {"kind": {"title": "Kind", "default": "link", "enum": ["link"], "type": "string"}, "description": {"title": "Description", "type": "string"}, "spec": {"$ref": "#/definitions/LinkTagSpec"}}, "required": ["spec"], "additionalProperties": false}, "BinaryStruct": {"title": "BinaryStruct", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "tags": {"title": "Tags", "default": {}, "type": "object", "additionalProperties": {"discriminator": {"propertyName": "kind", "mapping": {"command": "#/definitions/CommandTag", "link": "#/definitions/LinkTag"}}, "anyOf": [{"$ref": "#/definitions/CommandTag"}, {"$ref": "#/definitions/LinkTag"}]}}, "selected": {"title": "Selected", "type": "string"}}, "required": ["name"], "additionalProperties": false}}}
63 changes: 63 additions & 0 deletions altb/migrator/migrator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import json
from copy import deepcopy
from typing import List, Type
from dataclasses import dataclass

from jsonschema import validate

from .versions import ConfigVersion
from .migrations.base_migration import BaseMigration
from .migrations.migration_001_initial import MigrationInitial
from .migrations.migration_002_remove_is_copy import MigrationRemoveIsCopy


@dataclass
class MigrationConfig:
version: ConfigVersion
migration_class: Type[BaseMigration]


migrations: List[MigrationConfig] = [
MigrationConfig(ConfigVersion.v0_0_0, MigrationInitial),
MigrationConfig(ConfigVersion.v0_1_0, MigrationRemoveIsCopy),
]


def is_migrated(current_version: ConfigVersion) -> bool:
return migrations[-1].version == current_version


def migration_index(version: ConfigVersion):
for i, config in enumerate(migrations):
if config.version == version:
return i

return -1


def migrate(config: dict) -> dict:
version: ConfigVersion
if 'version' not in config:
version = ConfigVersion.v0_0_0
else:
version = ConfigVersion(config['version'])

index = migration_index(version)

print(f"detected config version of - {version}")
current_config = deepcopy(config)
migrations_needed = migrations[index+1:]
print(f"{len(migrations_needed)} migrations needed")
for migration in migrations_needed:
print(f"\tmigrating config to - {migration.version}")
instance = migration.migration_class()
current_config = instance.migrate(current_config)
current_config['version'] = str(migration.version.value)

with open(instance.EXPECTED_SCHEMA) as f:
schema = json.load(f)

validate(instance=current_config, schema=schema)

print("done!")
return current_config
7 changes: 7 additions & 0 deletions altb/migrator/versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class ConfigVersion(str, Enum):
v0_0_0 = "0.0.0"
v0_1_0 = "0.1.0"
latest = v0_1_0
Loading