Skip to content

Commit

Permalink
Merge branch 'release/0.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
arcticicestudio committed Jan 7, 2017
2 parents c439ddd + 5386033 commit fbbba49
Show file tree
Hide file tree
Showing 22 changed files with 858 additions and 6 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# + PyCharm +
# +---------+
# https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
out/
.idea/*
!.idea/runConfigurations
!.idea/misc.xml
Expand Down
40 changes: 39 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
<p align="center"><img src="https://cdn.rawgit.com/arcticicestudio/snowsaw/develop/assets/snowsaw-banner.svg"/></p>

<p align="center"><img src="https://assets-cdn.github.com/favicon.ico" width=24 height=24/> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/github/release/arcticicestudio/snowsaw.svg"/></a> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/badge/pre--release---_-blue.svg"/></a></p>
<p align="center"><img src="https://assets-cdn.github.com/favicon.ico" width=24 height=24/> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/github/release/arcticicestudio/snowsaw.svg"/></a> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/badge/pre--release---_-blue.svg"/></a> <img src="https://www.python.org/static/favicon.ico" width=24 height=24/> <img src="https://img.shields.io/badge/Python-3.5+-blue.svg"/></p>

---

# 0.1.0
*2017-01-07*
## Features
❯ Implemented the [CLI][readme-cli] (@arcticicestudio, #7, 35584e0e) and public [Plugin API][readme-plugin-api] (@arcticicestudio, #6, 7bee974a).

❯ Implemented the snowsaw core logic classes
- [`snowsaw.ConfigReader`](https://github.com/arcticicestudio/snowsaw/blob/develop/snowsaw/config.py) (@arcticicestudio, #1, bc9468df)
- [`snowsaw.Context`](https://github.com/arcticicestudio/snowsaw/blob/develop/snowsaw/context.py) (@arcticicestudio, #2, 528d1710)
- [`snowsaw.Dispatcher`](https://github.com/arcticicestudio/snowsaw/blob/develop/snowsaw/dispatcher.py) (@arcticicestudio, #5, 5bb0873a)

This includes the custom logger class [`snowsaw.logging.Logger`](https://github.com/arcticicestudio/snowsaw/blob/develop/snowsaw/logging/logger.py) (@arcticicestudio, #3, c56a7195) and the [`util`](https://github.com/arcticicestudio/snowsaw/tree/develop/snowsaw/util) (@arcticicestudio, #4, 695f1fd3) package which provides project util methods and classes.

❯ Implemented the [`setup.py`](https://github.com/arcticicestudio/snowsaw/blob/develop/snowsaw/setup.py) file. (@arcticicestudio, #8, 4fad0759)

❯ Implemented the core plugins
- [Clean][readme-core-tasks-clean] (@arcticicestudio, #9, 7fa022fd)
- [Link][readme-core-tasks-link] (@arcticicestudio, #10, 0cfd0b94)
- [Shell][readme-core-tasks-shell] (@arcticicestudio, #11, a51b61ba)

❯ Implemented the main snowsaw executeable binary [`bin/snowsaw`](https://github.com/arcticicestudio/snowsaw/blob/develop/bin/snowsaw). (@arcticicestudio, #12, 91b9febe)

A detailed [integration guide][readme-integration-guide] and more information about the public [Plugin API][readme-plugin-api], the [design concept][readme-design-concept] and the [configuration documentation][readme-configuration-documentation] can be found in the [README][readme] and the [project wiki][wiki].

# 0.0.0
*2017-01-07*
**Project Initialization**

[readme]: https://github.com/arcticicestudio/snowsaw/blob/develop/README.md
[readme-cli]: https://github.com/arcticicestudio/snowsaw#cli
[readme-configuration-documentation]: https://github.com/arcticicestudio/snowsaw#configuration
[readme-design-concept]: https://github.com/arcticicestudio/snowsaw#design-concept
[readme-integration-guide]: https://github.com/arcticicestudio/snowsaw#integration
[readme-plugin-api]: https://github.com/arcticicestudio/snowsaw#plugin-api
[readme-core-tasks-link]: https://github.com/arcticicestudio/snowsaw#link
[readme-core-tasks-clean]: https://github.com/arcticicestudio/snowsaw#clean
[readme-core-tasks-shell]: https://github.com/arcticicestudio/snowsaw#shell
[wiki]: https://github.com/arcticicestudio/snowsaw/wiki
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center"><img src="https://cdn.rawgit.com/arcticicestudio/snowsaw/develop/assets/snowsaw-banner.svg"/></p>

<p align="center"><img src="https://assets-cdn.github.com/favicon.ico" width=24 height=24/> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/github/release/arcticicestudio/snowsaw.svg"/></a> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/badge/pre--release---_-blue.svg"/></a></p>
<p align="center"><img src="https://assets-cdn.github.com/favicon.ico" width=24 height=24/> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/github/release/arcticicestudio/snowsaw.svg"/></a> <a href="https://github.com/arcticicestudio/snowsaw/releases/latest"><img src="https://img.shields.io/badge/pre--release---_-blue.svg"/></a> <img src="https://www.python.org/static/favicon.ico" width=24 height=24/> <img src="https://img.shields.io/badge/Python-3.5+-blue.svg"/></p>

<p align="center">A lightweight, plugin-driven and simple configurable dotfile bootstrapper.</p>

Expand All @@ -22,7 +22,7 @@ git submodule add https://github.com/arcticicestudio/snowsaw .snowsaw
This command will add the snowsaw project at the main development branch `develop`, but it is recommened to use a stable release version by running
```sh
cd .snowsaw
git checkout v0.0.0
git checkout v0.1.0
cd ..
```
and commit the changes in your dotfile repository to lock it on the specified version tag.
Expand Down Expand Up @@ -62,7 +62,7 @@ cd ..
```

## Design Concept
### <img src="assets/icon-snowblocks.svg"/> snowblocks
### <img src="https://cdn.rawgit.com/arcticicestudio/snowsaw/develop/assets/icon-snowblocks.svg"/> snowblocks
A `snowblock` is a named directory that represents a topic area.
Every valid `snowblock` contains a `snowblock.json` configuration file.
All `snowblock` directories are placed in one base directory, defaults to `<DOTFILE_REPOSITORY_ROOT>/snowblocks`, which will be processed recursively.
Expand Down Expand Up @@ -267,7 +267,7 @@ Defaults are specified as a dictionary mapping action names to settings, which a
```

## Development
[![](https://img.shields.io/badge/Changelog-0.0.0-blue.svg)](https://github.com/arcticicestudio/snowsaw/blob/v0.0.0/CHANGELOG.md) [![](https://img.shields.io/badge/Workflow-gitflow--branching--model-blue.svg)](http://nvie.com/posts/a-successful-git-branching-model) [![](https://img.shields.io/badge/Versioning-ArcVer_0.8.0-blue.svg)](https://github.com/arcticicestudio/arcver)
[![](https://img.shields.io/badge/Changelog-0.1.0-blue.svg)](https://github.com/arcticicestudio/snowsaw/blob/v0.1.0/CHANGELOG.md) [![](https://img.shields.io/badge/Workflow-gitflow--branching--model-blue.svg)](http://nvie.com/posts/a-successful-git-branching-model) [![](https://img.shields.io/badge/Versioning-ArcVer_0.8.0-blue.svg)](https://github.com/arcticicestudio/arcver)

### Contribution
Please report issues/bugs, feature requests and suggestions for improvements to the [issue tracker](https://github.com/arcticicestudio/snowsaw/issues).
Expand Down
21 changes: 21 additions & 0 deletions bin/snowsaw
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python

import sys
import os

PROJECT_ROOT_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))

if os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'snowsaw')):
if PROJECT_ROOT_DIRECTORY not in sys.path:
sys.path.insert(0, PROJECT_ROOT_DIRECTORY)
os.putenv('PYTHONPATH', PROJECT_ROOT_DIRECTORY)

import snowsaw


def main():
snowsaw.cli.main()


if __name__ == '__main__':
main()
2 changes: 2 additions & 0 deletions snowsaw/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .cli import main
from .plugin import Plugin
106 changes: 106 additions & 0 deletions snowsaw/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
The snowsaw CLI.
This is the main entry point of the public API.
"""
from argparse import ArgumentParser
import glob
import os

from .config import ConfigReader, ReadingError
from .dispatcher import Dispatcher, DispatchError
from .logging import Level
from .logging import Logger
from .util import module


def add_options(parser):
"""
Adds all options to the specified parser.
:param parser: The parser to add all options to
:return: None
"""
parser.add_argument('-Q', '--super-quiet', dest='super_quiet', action='store_true', help='suppress almost all output')
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', help='suppress most output')
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='enable verbose output')
parser.add_argument('-s', '--snowblocks-directory', nargs=1, dest='snowblocks_directory',
help='base snowblock directory to run all tasks of', metavar='SNOWBLOCKSDIR', required=True)
parser.add_argument('-c', '--config-file', nargs=1, dest='config_file', help='run tasks for the specified snowblock', metavar='CONFIGFILE')
parser.add_argument('-p', '--plugin', action='append', dest='plugins', default=[], help='load PLUGIN as a plugin', metavar='PLUGIN')
parser.add_argument('--disable-core-plugins', dest='disable_core_plugins', action='store_true', help='disable all core plugins')
parser.add_argument('--plugin-dir', action='append', dest='plugin_dirs', default=[], metavar='PLUGIN_DIR', help='load all plugins in PLUGIN_DIR')


def read_config(config_file):
"""
Reads the specified configuration file.
:param config_file: The configuration file to read
:return: The read configuration data
"""
reader = ConfigReader(config_file)
return reader.get_config()


def main():
"""
Processes all parsed options and hands it over to the dispatcher for each snowblock.
:return: True if all tasks have been executed successfully, False otherwise
"""
log = Logger()
try:
parser = ArgumentParser()
snowblock_config_filename = "snowblock.json"
add_options(parser)
options = parser.parse_args()

if options.super_quiet:
log.set_level(Level.WARNING)
if options.quiet:
log.set_level(Level.INFO)
if options.verbose:
log.set_level(Level.DEBUG)

plugin_directories = list(options.plugin_dirs)
if not options.disable_core_plugins:
plugin_directories.append(os.path.join(os.path.dirname(__file__), "plugins"))
plugin_paths = []
for directory in plugin_directories:
for plugin_path in glob.glob(os.path.join(directory, "*.py")):
plugin_paths.append(plugin_path)
for plugin_path in options.plugins:
plugin_paths.append(plugin_path)
for plugin_path in plugin_paths:
abspath = os.path.abspath(plugin_path)
module.load(abspath)

if options.config_file:
snowblocks = [os.path.basename(os.path.dirname(options.config_file[0]))]
else:
snowblocks = [snowblock for snowblock in os.listdir(options.snowblocks_directory[0])
if os.path.isdir(os.path.join(options.snowblocks_directory[0], snowblock))]

for snowblock in snowblocks:
if os.path.isfile(os.path.join(snowblock, snowblock_config_filename)):
log.info("❄ {}".format(snowblock))
tasks = read_config(os.path.join(snowblock, snowblock_config_filename))

if not isinstance(tasks, list):
raise ReadingError("Configuration file must be a list of tasks")

dispatcher = Dispatcher(snowblock)
success = dispatcher.dispatch(tasks)
if success:
log.info("==> All tasks executed successfully\n")
else:
raise DispatchError("\n==> Some tasks were not executed successfully")
else:
log.lowinfo("Skipped snowblock \"{}\": No configuration file found".format(snowblock))
except (ReadingError, DispatchError) as e:
log.error("{}".format(e))
exit(1)
except KeyboardInterrupt:
log.error("\n==> Operation aborted")
exit(1)
37 changes: 37 additions & 0 deletions snowsaw/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import json
import os.path


class ConfigReader(object):
"""
Snowblock configuration file reader.
"""
def __init__(self, config_file_path):
self._config = self._read(config_file_path)

def _read(self, config_file_path):
"""
Reads the specified configuration file.
:param config_file_path: The path to the configuration file to read
:return: The read configuration data
"""
try:
_, ext = os.path.splitext(config_file_path)
with open(config_file_path) as fin:
data = json.load(fin)
return data
except Exception as e:
raise ReadingError('Could not read config file:\n{}'.format(e))

def get_config(self):
"""
Gets the snowblock configuration data.
:return: The read snowblock configuration data
"""
return self._config


class ReadingError(Exception):
pass
23 changes: 23 additions & 0 deletions snowsaw/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import copy


class Context(object):
"""
Contextual data and information for plugins
"""
def __init__(self, snowblock_dir):
self._snowblock_dir = snowblock_dir
self._defaults = {}
pass

def set_snowblock_dir(self, snowblock_dir):
self._snowblock_dir = snowblock_dir

def snowblock_dir(self):
return self._snowblock_dir

def set_defaults(self, defaults):
self._defaults = defaults

def defaults(self):
return copy.deepcopy(self._defaults)
64 changes: 64 additions & 0 deletions snowsaw/dispatcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os
from .plugin import Plugin
from .logging import Logger
from .context import Context


class Dispatcher(object):
"""
Dispatches tasks to loaded plugins.
"""
def __init__(self, snowblock_dir):
self._log = Logger()
self._setup_context(snowblock_dir)
self._load_plugins()

def _setup_context(self, snowblock_dir):
"""
Sets up the plugin context for the specified snowblock.
:param snowblock_dir: The directory of the snowblock
:return: The plugin context
"""
path = os.path.abspath(os.path.realpath(os.path.expanduser(snowblock_dir)))
if not os.path.exists(path):
raise DispatchError("Nonexistent snowblock directory")
self._context = Context(path)

def dispatch(self, tasks):
"""
Dispatches the specified tasks to the loaded plugins.
:param tasks: The tasks to dispatch
:return: True if all tasks have been handled by the loaded plugins successfully, False otherwise
"""
success = True
for task in tasks:
for action in task:
handled = False
if action == "defaults":
self._context.set_defaults(task[action])
handled = True
for plugin in self._plugins:
if plugin.can_handle(action):
try:
success &= plugin.handle(action, task[action])
handled = True
except Exception:
self._log.error("An error was encountered while executing action \"{}\"".format(action))
if not handled:
success = False
self._log.error("Action \"{}\" not handled".format(action))
return success

def _load_plugins(self):
"""
Loads all found plugins.
:return: None
"""
self._plugins = [plugin(self._context) for plugin in Plugin.__subclasses__()]


class DispatchError(Exception):
pass
6 changes: 6 additions & 0 deletions snowsaw/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Provides a custom logging class with level color support.
"""
from .logger import Logger
from .color import Color
from .level import Level
11 changes: 11 additions & 0 deletions snowsaw/logging/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Color(object):
"""
Terminal escape sequences for colored logging.
"""
NONE = ""
RESET = "\033[0m"
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
10 changes: 10 additions & 0 deletions snowsaw/logging/level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Level(object):
"""
Logging level names as numeric values.
"""
NOTSET = 0
DEBUG = 10
LOWINFO = 15
INFO = 20
WARNING = 30
ERROR = 40
Loading

0 comments on commit fbbba49

Please sign in to comment.