-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 083fd01
Showing
28 changed files
with
2,341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
recursive-include tests *.py | ||
recursive-include docs *.rst *.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
""" |
Oops, something went wrong.