-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feeds #16
Feeds #16
Changes from all commits
ae2ef0b
1bea78b
e02f519
76b7739
ba6a996
36bbabf
a3352a0
7079d5b
f83cf76
8254e54
ee634af
58d2723
2e5398d
64ecec6
87b710c
6526f06
8881e33
29fb2af
8b5b93a
12aeee9
f366c6c
b9f1924
62eeacf
fd73449
0446c06
ae33115
e485628
606b673
473539d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
import jsonschema.exceptions | ||
|
||
from routemaster.config.model import ( | ||
Feed, | ||
Gate, | ||
State, | ||
Action, | ||
|
@@ -106,15 +107,27 @@ def _load_state_machine( | |
name: str, | ||
yaml_state_machine: Yaml, | ||
) -> StateMachine: | ||
feeds = [_load_feed(x) for x in yaml_state_machine.get('feeds', [])] | ||
|
||
if len(set(x.name for x in feeds)) < len(feeds): | ||
raise ConfigError( | ||
f"Feeds must have unique names at {'.'.join(path + ['feeds'])}", | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For even nicer errors, we could use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd expect it will be typically 1, sometimes 1<n<5. I don't think this is necessary for now. |
||
|
||
return StateMachine( | ||
name=name, | ||
states=[ | ||
_load_state(path + ['states', str(idx)], yaml_state) | ||
for idx, yaml_state in enumerate(yaml_state_machine['states']) | ||
], | ||
feeds=feeds, | ||
) | ||
|
||
|
||
def _load_feed(yaml: Yaml) -> Feed: | ||
return Feed(name=yaml['name'], url=yaml['url']) | ||
|
||
|
||
def _load_state(path: Path, yaml_state: Yaml) -> State: | ||
if 'action' in yaml_state and 'gate' in yaml_state: # pragma: no branch | ||
raise ConfigError( # pragma: no cover | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,10 @@ | |
NamedTuple, | ||
) | ||
|
||
from routemaster.exit_conditions import Context, ExitConditionProgram | ||
from routemaster.exit_conditions import ExitConditionProgram | ||
|
||
if False: # typing | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the rationale behind this being a fake import -- is it circular? I'm ok with using the (future occurrences not commented) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes This is only to support typing, and in Python 3.7 we can switch to 'if TYPING'. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, could we have a comment about these being circular? (possibly as well as the need for it only being for typing). |
||
from routemaster.context import Context # noqa | ||
|
||
|
||
class TimeTrigger(NamedTuple): | ||
|
@@ -69,7 +72,7 @@ class ContextNextStates(NamedTuple): | |
path: str | ||
destinations: Iterable[ContextNextStatesOption] | ||
|
||
def next_state_for_label(self, label_context: Context) -> str: | ||
def next_state_for_label(self, label_context: 'Context') -> str: | ||
"""Returns next state based on context value at `self.path`.""" | ||
val = label_context.lookup(self.path.split('.')) | ||
for destination in self.destinations: | ||
|
@@ -133,10 +136,19 @@ class Action(NamedTuple): | |
State = Union[Action, Gate] | ||
|
||
|
||
class Feed(NamedTuple): | ||
""" | ||
The definition of a feed of dynamic data to be included in a context. | ||
""" | ||
name: str | ||
url: str | ||
|
||
|
||
class StateMachine(NamedTuple): | ||
"""A state machine.""" | ||
name: str | ||
states: List[State] | ||
feeds: List[Feed] | ||
|
||
def get_state(self, state_name: str) -> State: | ||
"""Get the state object for a given state name.""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
"""Context definition for exit condition programs.""" | ||
import datetime | ||
from typing import Any, Dict, Iterable, Sequence | ||
|
||
from routemaster.feeds import Feed | ||
from routemaster.utils import get_path | ||
|
||
|
||
class Context(object): | ||
"""Execution context for exit condition programs.""" | ||
|
||
def __init__( | ||
self, | ||
label: str, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Elsewhere (in the API at least) we call this a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I'm not sure about this. A label should be just the string, I think we should probably change the |
||
metadata: Dict[str, Any], | ||
now: datetime.datetime, | ||
feeds: Dict[str, Feed], | ||
accessed_variables: Iterable[str], | ||
) -> None: | ||
"""Create an execution context.""" | ||
if now.tzinfo is None: | ||
raise ValueError( | ||
"Cannot evaluate exit conditions with naive datetimes", | ||
) | ||
|
||
self.now = now | ||
self.metadata = metadata | ||
self.feeds = feeds | ||
|
||
self._pre_warm_feeds(label, accessed_variables) | ||
|
||
def lookup(self, path: Sequence[str]) -> Any: | ||
"""Look up a path in the execution context.""" | ||
location, *rest = path | ||
|
||
try: | ||
return { | ||
'metadata': self._lookup_metadata, | ||
'feeds': self._lookup_feed_data, | ||
}[location](rest) | ||
except (KeyError, ValueError): | ||
return None | ||
|
||
def _lookup_metadata(self, path: Sequence[str]) -> Any: | ||
return get_path(path, self.metadata) | ||
|
||
def _lookup_feed_data(self, path: Sequence[str]) -> Any: | ||
feed_name, *rest = path | ||
return self.feeds[feed_name].lookup(rest) | ||
|
||
def property_handler(self, property_name, value, **kwargs): | ||
"""Handle a property in execution.""" | ||
if property_name == ('passed',): | ||
epoch = kwargs['since'] | ||
return (self.now - epoch).total_seconds() >= value | ||
if property_name == ('defined',): | ||
return value is not None | ||
if property_name == () and 'in' in kwargs: | ||
return value in kwargs['in'] | ||
raise ValueError("Unknown property {name}".format( | ||
name='.'.join(property_name)), | ||
) | ||
|
||
def _pre_warm_feeds(self, label: str, accessed_variables: Iterable[str]): | ||
for accessed_variable in accessed_variables: | ||
parts = accessed_variable.split('.') | ||
|
||
if len(parts) < 2: | ||
continue | ||
|
||
if parts[0] != 'feeds': | ||
continue | ||
|
||
feed = self.feeds.get(parts[1]) | ||
if feed is not None: | ||
feed.prefetch(label) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,7 @@ | ||
"""Parsing and evaluation of exit condition programs.""" | ||
|
||
from routemaster.exit_conditions.context import Context | ||
from routemaster.exit_conditions.program import ExitConditionProgram | ||
|
||
__all__ = ( | ||
'Context', | ||
'ExitConditionProgram', | ||
) |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 to adding a typechecking requirements.txt; this change (and the similar one below) should probably be cherry-picked to master though.