Skip to content

Commit

Permalink
mvp: status, log and apply
Browse files Browse the repository at this point in the history
  • Loading branch information
ynohat committed Sep 7, 2020
0 parents commit 894fa16
Show file tree
Hide file tree
Showing 22 changed files with 906 additions and 0 deletions.
129 changes: 129 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/
57 changes: 57 additions & 0 deletions bossman/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from os.path import abspath, commonpath, join

from bossman.resources import ResourceManager, ResourceStatus
from bossman.abc.resource_type import ResourceTypeABC
from bossman.abc.resource import ResourceABC
from bossman.changes import Change, ChangeSet
from bossman.config import Config, ResourceTypeConfig
from bossman.logging import get_class_logger
from bossman.repo import Repo


class Bossman:
def __init__(self, repo_path, config: Config):
self.config = config
self.repo = Repo(repo_path)
self.resource_manager = ResourceManager()
for config in self.config.resource_types:
resource_type = ResourceTypeABC.create(config)
self.resource_manager.register_resource_type(resource_type)
self.logger = get_class_logger(self)

def get_resources(self, rev: str = "HEAD") -> list:
return self.resource_manager.get_resources(self.repo, rev)

def get_resource_status(self, resource: ResourceABC) -> ResourceStatus:
resource_type = self.resource_manager.get_resource_type(resource.path)
local_rev = self.repo.get_last_change_rev(resource.paths)
remote_rev = resource_type.get_remote_rev(resource)
missing_changesets = self.get_changesets(remote_rev, local_rev, [resource])
dirty = resource_type.is_dirty(resource)
return ResourceStatus(
local_rev=local_rev,
remote_rev=remote_rev,
dirty=dirty,
missing_changesets=missing_changesets
)

def get_changesets(self, since_rev: str = None, until_rev: str = "HEAD", resources: list = None) -> str:
from collections import defaultdict
resources = resources if resources else self.get_resources()
paths = [resource.path for resource in resources]
commits = self.repo.get_commits(since_rev, until_rev, paths)
changeSets = []
for commit in commits:
changeSet = ChangeSet(commit)
changeSets.append(changeSet)
resources = dict()
for diff in commit.diffs:
the_path = diff.b_path or diff.a_path
resource = self.resource_manager.get_resource(the_path)
if resource:
changeSet.add_resource_diff(resource, diff)
return changeSets

def apply_change(self, changeset: ChangeSet, change: Change):
resource_type = self.resource_manager.get_resource_type(change.resource.path)
resource_type.apply(changeset, change)
Empty file added bossman/abc/__init__.py
Empty file.
23 changes: 23 additions & 0 deletions bossman/abc/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from abc import ABC, abstractproperty

class ResourceABC(ABC):
"""
A {ResourceABC} is just a thing that lives at a path.
"""
def __init__(self, path):
self.path = path

@abstractproperty
def paths(self):
pass

def __eq__(self, other):
if isinstance(other, ResourceABC):
return self.path == other.path
return False

def __hash__(self):
return hash(self.path)

def __str__(self):
return self.path
79 changes: 79 additions & 0 deletions bossman/abc/resource_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from abc import ABC, abstractmethod
from bossman.config import ResourceTypeConfig
from bossman.changes import Change, ChangeSet
from bossman.logging import logger
from bossman.abc.resource import ResourceABC
import parse
from os.path import relpath, join

class ResourceTypeABC(ABC):
@staticmethod
def create(config: ResourceTypeConfig):
import importlib
plugin = importlib.import_module(config.module)
return plugin.ResourceType(config)

"""
Abstract class for resource_types.
"""
def __init__(self, config: ResourceTypeConfig):
self.config = config
self.logger = logger.getChild(
"{module_name}.{class_name}".format(
module_name=self.__class__.__module__,
class_name=self.__class__.__name__
)
)
self.logger.info("config={config}".format(config=str(config)))

def get_resources(self, paths: list):
"""
Given a list of paths relative to the repo, determine the subset managed
by this resource type and return the list of {ResourceABC}s.
"""
return set(
resource
for resource
in (
self.get_resource(path)
for path
in paths
)
if resource != None
)

def match(self, path: str) -> bool:
result = parse.search(self.config.pattern, path)
if result:
canonical = self.config.pattern.format(**result.named)
return path.startswith(canonical)
return False

def get_resource(self, path: str) -> ResourceABC:
result = parse.search(self.config.pattern, path)
if result:
canonical = self.config.pattern.format(**result.named)
if path.startswith(canonical):
return self.create_resource(canonical, **result.named)
return None

def describe_diffs(self, resource: ResourceABC, diffs: list) -> Change:
change = Change(resource)
change.diffs = diffs
return change

@abstractmethod
def create_resource(self, path, **kwargs) -> ResourceABC:
pass

@abstractmethod
def get_remote_rev(self, resource: ResourceABC) -> str:
pass

@abstractmethod
def is_dirty(self, resource: ResourceABC) -> bool:
pass

@abstractmethod
def apply(self, changeset: ChangeSet, change: Change) -> bool:
pass
45 changes: 45 additions & 0 deletions bossman/changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from os.path import basename
from bossman.repo import Commit, Diff
from bossman.abc.resource import ResourceABC
from collections import OrderedDict

class ChangeSet:
def __init__(self, commit: Commit):
self.rev = commit.rev
self.message = commit.message
self.author = commit.author
self.date = commit.date
self.resource_changes = OrderedDict()

def add_resource_diff(self, resource: ResourceABC, diff: Diff):
if not (resource in self.resource_changes):
self.resource_changes[resource] = Change(resource)
change = self.resource_changes.get(resource)
change.diffs.append(diff)

def __str__(self):
s = "[{rev}] {date} {message} | {author}".format(rev=self.rev, message=self.message.split("\n")[0], author=self.author, date=self.date)
if len(self.resource_changes):
s += "\n\n "
s += "\n ".join(str(change) for change in self.resource_changes.values())
s += "\n"
return s

class Change:
def __init__(self, resource: ResourceABC):
self.resource = resource
self.diffs = []

def __str__(self):
s = "{resource:<58} (".format(resource=str(self.resource))
s += ", ".join(map(format_diff, self.diffs))
s += ")"
return s

def format_diff(diff):
if diff.change_type in 'D':
return diff.change_type + " " + basename(diff.a_path)
elif diff.change_type == 'R':
return diff.change_type + " " + basename(diff.a_path) + " -> " + basename(diff.b_path)
else:
return diff.change_type + " " + basename(diff.b_path)
52 changes: 52 additions & 0 deletions bossman/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import sys
from os import path, getcwd
import yaml
import argparse
from bossman.cli import status_cmd, log_cmd, apply_cmd
from bossman.config import Config
from bossman.logging import logger
from bossman import Bossman
from bossman.config import Config

logger = logger.getChild(globals().get("__name__"))

def main():
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", action=SetVerbosity, default="ERROR", type=str, choices=["DEBUG", "INFO", "WARNING", "ERROR"])
parser.add_argument("--repo", help="path to the repository", default=getcwd())

subparsers = parser.add_subparsers(title="Subcommands")
status_cmd.init(subparsers)
log_cmd.init(subparsers)
apply_cmd.init(subparsers)

args = parser.parse_args()
bossman = create_bossman(args)

if "func" in args:
args.func(bossman, **vars(args))
else:
parser.print_usage()

def create_bossman(args):
conf_path = path.join(args.repo, ".bossman")
if path.exists(conf_path):
fd = open(conf_path, "r")
conf_data = yaml.safe_load(fd)

config = Config(conf_data)

bossman = Bossman(args.repo, config)
return bossman

class SetVerbosity(argparse.Action):
"""
Set logger verbosity when --verbosity argument is provided.
"""
def __init__(self, *args, **kwargs):
super(SetVerbosity, self).__init__(*args, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
import logging
logging.basicConfig(level=values)
logger.info("SetVerbosity verbosity={verbosity}".format(verbosity=values))
Loading

0 comments on commit 894fa16

Please sign in to comment.