Skip to content

Commit

Permalink
Initial code
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Aug 26, 2015
0 parents commit 083fd01
Show file tree
Hide file tree
Showing 28 changed files with 2,341 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.project
.pydevproject
.idea
.tox
.coverage
.cache
.eggs/
*.egg-info/
*.pyc
__pycache__/
docs/_build/
dist/
build/
11 changes: 11 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
language: python
python: 3.4
env:
- TOX_ENV=py34
- TOX_ENV=py35
- TOX_ENV=docs
- TOX_ENV=flake8
install:
- pip install tox
script:
- tox -e $TOX_ENV
13 changes: 13 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2015 Alex Grönholm

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
recursive-include tests *.py
recursive-include docs *.rst *.py
34 changes: 34 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Asphalt is a microframework for service oriented applications.

It consists of the core (this) and an ecosystem of high level components that offer additional
functionality, often by integrating third party libraries. Asphalt is unique in that it allows
developers to use both coroutine-based (asynchronous) and traditional blocking programming styles
in the same application and every API provided by any Asphalt component supports both approaches,
nearly transparently to the developer.

Asphalt is based on the standard library `asyncio`_ module and requires Python 3.4 or later.

The list of `available components`_ is on the Asphalt wiki.


Important links
---------------

* `Documentation`_
* `Source code`_
* `Issue tracker`_


Support
-------

* `Freenode IRC`_: #asphalt
* `StackOverflow`_: Tag your questions with ``asphalt``

.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _available components: https://github.com/asphalt-framework/asphalt/wiki/Components
.. _Documentation: http://asphalt.readthedocs.org/en/latest/
.. _Source code: https://github.com/asphalt-framework/asphalt
.. _Issue tracker: https://github.com/asphalt-framework/asphalt/issues
.. _Freenode IRC: https://freenode.net/
.. _StackOverflow: http://stackoverflow.com/questions/tagged/asphalt
Empty file added asphalt/core/__init__.py
Empty file.
112 changes: 112 additions & 0 deletions asphalt/core/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from asyncio import AbstractEventLoop, get_event_loop
from concurrent.futures import ThreadPoolExecutor
from logging import getLogger
from typing import Dict, Any, Union, List
import logging.config
import asyncio

from pkg_resources import iter_entry_points, EntryPoint

from .component import Component
from .context import ApplicationContext, ContextEventType
from .util import resolve_reference


class Application:
"""
This class orchestrates the configuration and setup of the chosen Asphalt components.
:param components: a dictionary of component identifier -> constructor arguments
:param max_threads: maximum number of worker threads
:param logging: logging configuration dictionary for :func:`~logging.config.dictConfig`
or a boolean (``True`` = call :func:`~logging.basicConfig`,
``False`` = skip logging configuration)
:param settings: application specific configuration options (available as ctx.settings)
"""

__slots__ = ('settings', 'component_types', 'component_options', 'max_threads',
'logging_config', 'logger')

def __init__(self, components: Dict[str, Dict[str, Any]]=None, *, max_threads: int=8,
logging: Union[Dict[str, Any], bool]=True, settings: dict=None):
assert max_threads > 0, 'max_threads must be a positive integer'
self.settings = settings
self.component_types = {ep.name: ep for ep in iter_entry_points('asphalt.components')}
self.component_options = components or {}
self.max_threads = max_threads
self.logging_config = logging
self.logger = getLogger('asphalt.core')

def create_context(self) -> ApplicationContext:
return ApplicationContext(self.settings)

def create_components(self) -> List[Component]:
components = []
for name, config in self.component_options.items():
assert isinstance(config, dict)
if 'class' in config:
component_class = resolve_reference(config.pop('class'))
elif name in self.component_types:
component_class = self.component_types[name]
if isinstance(component_class, EntryPoint):
component_class = component_class.load()
else:
raise LookupError('no such component: {}'.format(name))

assert issubclass(component_class, Component)
component = component_class(**config)
components.append(component)

return components

def start(self, app_ctx: ApplicationContext):
"""
This method should be overridden to implement custom application logic.
It is called after all the components have initialized.
It can be a coroutine.
"""

def run(self, event_loop: AbstractEventLoop=None):
# Configure the logging system
if isinstance(self.logging_config, dict):
logging.config.dictConfig(self.logging_config)
elif self.logging_config:
logging.basicConfig(level=logging.INFO)

# Assign a new default executor with the given max worker thread limit
event_loop = event_loop or get_event_loop()
event_loop.set_default_executor(ThreadPoolExecutor(self.max_threads))

# Create the application context
context = self.create_context()

try:
# Start all the components and run the loop until they've finished
self.logger.info('Starting components')
components = self.create_components()
coroutines = [coro for coro in (component.start(context) for component in components)
if coro is not None]
event_loop.run_until_complete(asyncio.gather(*coroutines))
self.logger.info('All components started')

# Run the application's custom startup code
coro = self.start(context)
if coro is not None:
event_loop.run_until_complete(coro)

# Run all the application context's start callbacks
event_loop.run_until_complete(context.run_callbacks(ContextEventType.started))
self.logger.info('Application started')
except Exception as exc:
self.logger.exception('Error during application startup')
context.exception = exc
else:
# Finally, run the event loop until the process is terminated or Ctrl+C is pressed
try:
event_loop.run_forever()
except (KeyboardInterrupt, SystemExit):
pass

event_loop.run_until_complete(context.run_callbacks(ContextEventType.finished))
event_loop.close()
self.logger.info('Application stopped')
141 changes: 141 additions & 0 deletions asphalt/core/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from pathlib import Path
from typing import Union
import argparse
import sys

import pkg_resources
import yaml

from .application import Application
from .context import ApplicationContext
from .util import resolve_reference


def quickstart_application():
"""Asks a few questions and builds a skeleton application directory structure."""

current_version = pkg_resources.get_distribution('asphalt').parsed_version.public
next_major_version = '{}.0.0'.format(int(current_version.split('.')[0]) + 1)

project_name = input('Project name: ')
package = input('Top level package name: ')
app_subclass = '{}Application'.format(
''.join(part.capitalize() for part in project_name.split()))

project = Path(project_name)
if project.exists():
print('Error: the directory "{}" already exists.'.format(project), file=sys.stderr)
sys.exit(1)

top_package = project / package
top_package.mkdir(parents=True)
with (top_package / '__init__.py').open('w') as f:
pass

with (top_package / 'application.py').open('w') as f:
f.write("""\
from {app_cls.__module__} import {app_cls.__name__}
from {context_cls.__module__} import {context_cls.__name__}
class {app_subclass}({app_cls.__name__}):
@coroutine
def start(app_ctx: ApplicationContext):
pass # IMPLEMENT CUSTOM LOGIC HERE
""".format(app_cls=Application, context_cls=ApplicationContext, app_subclass=app_subclass))

with (project / 'config.yml').open('w') as f:
f.write("""\
---
application_class: {package}:{app_subclass}
components:
foo: {{}} # REPLACE ME
logging:
version: 1
disable_existing_loggers: false
handlers:
console:
class: logging.StreamHandler
formatter: generic
formatters:
generic:
format: "%(asctime)s:%(levelname)s:%(name)s:%(message)s"
root:
handlers: [console]
level: INFO
""".format(package=package, app_subclass=app_subclass))

with (project / 'setup.py').open('w') as f:
f.write("""\
from setuptools import setup
setup(
name='{package}',
version='1.0.0',
description='{project_name}',
long_description='FILL IN HERE',
author='FILL IN HERE',
author_email='FILL IN HERE',
classifiers=[
'Intended Audience :: End Users/Desktop',
'Programming Language :: Python',
'Programming Language :: Python :: 3'
],
zip_safe=True,
packages=[
'{package}'
],
install_requires=[
'asphalt >= {current_version}, < {next_major_version}'
]
)
""".format(package=package, project_name=project_name, current_version=current_version,
next_major_version=next_major_version))


def run_from_config_file(config_file: Union[str, Path], unsafe: bool=False):
"""
Runs an application using configuration from the given configuration file.
:param config_file: path to a YAML configuration file
:param unsafe: ``True`` to load the YAML file in unsafe mode
"""

# Read the configuration from the supplied YAML file
with Path(config_file).open() as stream:
config = yaml.load(stream) if unsafe else yaml.safe_load(stream)
assert isinstance(config, dict), 'the YAML document root must be a dictionary'

# Instantiate and run the application
application_class = resolve_reference(config.pop('application_class', Application))
application = application_class(**config)
application.run()


def main():
parser = argparse.ArgumentParser(description='The Asphalt application framework')
subparsers = parser.add_subparsers()

run_parser = subparsers.add_parser(
'run', help='Run an Asphalt application from a YAML configuration file')
run_parser.add_argument('config_file', help='Path to the configuration file')
run_parser.add_argument(
'--unsafe', action='store_true', default=False,
help='Load the YAML file in unsafe mode (enables YAML markup extensions)')
run_parser.set_defaults(func=run_from_config_file)

quickstart_parser = subparsers.add_parser(
'quickstart', help='Quickstart an Asphalt application'
)
quickstart_parser.set_defaults(func=quickstart_application)

args = parser.parse_args(sys.argv[1:])
if 'func' in args:
kwargs = dict(args._get_kwargs())
del kwargs['func']
args.func(**kwargs)
else:
parser.print_help()

if __name__ == '__main__': # pragma: no cover
main()
27 changes: 27 additions & 0 deletions asphalt/core/component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from abc import ABCMeta, abstractmethod

from .context import ApplicationContext


class Component(metaclass=ABCMeta):
"""This is the base class for all Asphalt components."""

__slots__ = ()

@abstractmethod
def start(self, app_ctx: ApplicationContext):
"""
This method is called on application start. It can be a coroutine.
The application context can be used to:
* add application start/finish callbacks
* add default callbacks for other contexts
* add context getters
* add resources (via app_ctx.resources)
* request resources (via app_ctx.resources)
When dealing with resources, it is advisable to add as many resources as possible
before requesting any. This will speed up the dependency resolution.
:param app_ctx: the application context
"""

0 comments on commit 083fd01

Please sign in to comment.