Skip to content
This repository has been archived by the owner on Jun 15, 2024. It is now read-only.

Commit

Permalink
Merge pull request #127 from timkpaine/rules
Browse files Browse the repository at this point in the history
starting on rules engine integration
  • Loading branch information
timkpaine authored Oct 21, 2020
2 parents 1b5f40f + 5cde727 commit f18a084
Show file tree
Hide file tree
Showing 11 changed files with 573 additions and 518 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,29 @@ date
## Demo
![](https://raw.githubusercontent.com/timkpaine/pyEX/main/docs/img/example1.gif)
## Full API
## Rules Engine
`pyEX` implements methods for interacting with the [Rules Engine](https://iexcloud.io/docs/api/#rules-engine-beta).
```python
rule = {
'conditions': [['changePercent','>',500],
['latestPrice','>',100000]],
'outputs': [{'frequency': 60,
'method': 'email',
'to': 'your_email@domain'
}]
}
c.createRule(rule, 'MyTestRule', 'AAPL', 'all') # returns {"id": <ruleID>, "weight": 2}
c.pauseRule("<ruleID>")
c.resumeRule("<ruleID>")
c.deleteRule("<ruleID>")
```
We also provide helper classes in python for constructing rules such that they abide by the rules schema (dictated in the `schema()` helper function)
## Data
### Full API
Please see the [readthedocs](https://pyEX.readthedocs.io) for a full API spec
![](https://raw.githubusercontent.com/timkpaine/pyEX/main/docs/img/rtd.png)
Expand Down
1 change: 1 addition & 0 deletions pyEX/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
internationalExchanges, internationalExchangesDF, # noqa: F401
sectors, sectorsDF, # noqa: F401
tags, tagsDF) # noqa: F401
from .rules import schema, create, lookup, pause, resume, delete # noqa: F401
from .stats import stats, statsDF, recent, recentDF, records, recordsDF, summary, summaryDF, daily, dailyDF # noqa: F401
from .stocks import (advancedStats, advancedStatsDF, # noqa: F401
analystRecommendations, analystRecommendationsDF, # noqa: F401
Expand Down
4 changes: 2 additions & 2 deletions pyEX/account/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def messageBudget(totalMessages=None, token='', version=''):
_requireSecret(token)
if not isinstance(totalMessages, int):
raise PyEXception("`totalMessages` must be integer, got {}({})".format(type(totalMessages), totalMessages))
return _postJson('account/messagebudget?totalMessages={}'.format(totalMessages), token, version)
return _postJson('account/messagebudget?totalMessages={}'.format(totalMessages), token=token, version=version)


def metadata(token='', version=''):
Expand Down Expand Up @@ -63,7 +63,7 @@ def payAsYouGo(allow=False, token='', version=''):
_requireSecret(token)
if not isinstance(allow, bool):
raise PyEXception("`allow` must be bool, got {}({})".format(type(allow), allow))
return _postJson('account/messagebudget?allow={}'.format(allow), token, version)
return _postJson('account/messagebudget?allow={}'.format(allow), token=token, version=version)


def usage(type=None, token='', version=''):
Expand Down
8 changes: 8 additions & 0 deletions pyEX/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
internationalExchanges, internationalExchangesDF, \
sectors, sectorsDF, \
tags, tagsDF
from .rules import schema, create, lookup, pause, resume, delete
from .stats import stats, statsDF, \
recent, recentDF, \
records, recordsDF, \
Expand Down Expand Up @@ -187,6 +188,13 @@
DEFAULT_API_LIMIT = 5

_INCLUDE_FUNCTIONS = [
# Rules
('schema', schema),
('createRule', create),
('lookupRule', lookup),
('pauseRule', pause),
('resumeRule', resume),
('deleteRule', delete),
# Refdata
('symbols', symbols),
('iexSymbols', iexSymbols),
Expand Down
47 changes: 40 additions & 7 deletions pyEX/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,18 @@ def _getJson(url, token='', version='', filter=''):
return _getJsonOrig(url)


def _postJson(url, token='', version=''):
def _postJson(url, data=None, json=None, token='', version='', token_in_params=True):
token = token or os.environ.get('IEX_TOKEN')
if version == 'sandbox':
return _postJsonIEXCloudSandbox(url, token, version)
return _postJsonIEXCloud(url, token, version)
return _postJsonIEXCloudSandbox(url, data, json, token, version, token_in_params)
return _postJsonIEXCloud(url, data, json, token, version, token_in_params)


def _deleteJson(url, token='', version=''):
token = token or os.environ.get('IEX_TOKEN')
if version == 'sandbox':
return _deleteJsonIEXCloudSandbox(url, token, version)
return _deleteJsonIEXCloud(url, token, version)


def _getJsonOrig(url):
Expand All @@ -409,11 +416,24 @@ def _getJsonIEXCloud(url, token='', version='v1', filter=''):
raise PyEXception('Response %d - ' % resp.status_code, resp.text)


def _postJsonIEXCloud(url, token='', version='v1'):
def _postJsonIEXCloud(url, data=None, json=None, token='', version='v1', token_in_params=True):
'''for iex cloud'''
url = _URL_PREFIX2.format(version=version) + url
if token_in_params:
params = {'token': token}
else:
params = {}
resp = requests.post(urlparse(url).geturl(), data=data, json=json, proxies=_PYEX_PROXIES, params=params)
if resp.status_code == 200:
return resp.json()
raise PyEXception('Response %d - ' % resp.status_code, resp.text)


def _deleteJsonIEXCloud(url, token='', version='v1'):
'''for iex cloud'''
url = _URL_PREFIX2.format(version=version) + url
params = {'token': token}
resp = requests.post(urlparse(url).geturl(), proxies=_PYEX_PROXIES, params=params)
resp = requests.delete(urlparse(url).geturl(), proxies=_PYEX_PROXIES, params=params)
if resp.status_code == 200:
return resp.json()
raise PyEXception('Response %d - ' % resp.status_code, resp.text)
Expand All @@ -431,11 +451,24 @@ def _getJsonIEXCloudSandbox(url, token='', version='v1', filter=''):
raise PyEXception('Response %d - ' % resp.status_code, resp.text)


def _postJsonIEXCloudSandbox(url, token='', version='v1'):
def _postJsonIEXCloudSandbox(url, data=None, json=None, token='', version='v1', token_in_params=True):
'''for iex cloud'''
url = _URL_PREFIX2_SANDBOX.format(version='v1') + url
if token_in_params:
params = {'token': token}
else:
params = {}
resp = requests.post(urlparse(url).geturl(), data=data, json=json, proxies=_PYEX_PROXIES, params=params)
if resp.status_code == 200:
return resp.json()
raise PyEXception('Response %d - ' % resp.status_code, resp.text)


def _deleteJsonIEXCloudSandbox(url, token='', version='v1'):
'''for iex cloud'''
url = _URL_PREFIX2_SANDBOX.format(version='v1') + url
params = {'token': token}
resp = requests.post(urlparse(url).geturl(), proxies=_PYEX_PROXIES, params=params)
resp = requests.delete(urlparse(url).geturl(), proxies=_PYEX_PROXIES, params=params)
if resp.status_code == 200:
return resp.json()
raise PyEXception('Response %d - ' % resp.status_code, resp.text)
Expand Down
98 changes: 98 additions & 0 deletions pyEX/rules/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
from functools import wraps
from ..common import _getJson, _postJson, _deleteJson, _raiseIfNotStr, PyEXception
from .engine import Rule # noqa: F401


def lookup(lookup='', token='', version=''):
'''Pull the latest schema for data points, notification types, and operators used to construct rules.
https://iexcloud.io/docs/api/#rules-schema
Args:
lookup (string); If a schema object has “isLookup”: true, pass the value key to /stable/rules/lookup/{value}. This returns all valid values for the rightValue of a condition.
token (string); Access token
version (string); API version
Returns:
dict: result
'''
_raiseIfNotStr(lookup)
if lookup:
return _getJson('rules/lookup/{}'.format(lookup), token, version, None)
return _getJson('rules/schema', token, version, None)


@wraps(lookup)
def schema(token='', version=''):
return lookup(token=token, version=version)


def create(rule, ruleName, ruleSet, type='any', existingId=None, token='', version=''):
'''This endpoint is used to both create and edit rules. Note that rules run be default after being created.
Args:
rule (Rule or dict): rule object to create
ruleName (str): name for rule
ruleSet (str): Valid US symbol or the string ANYEVENT. If the string ANYEVENT is passed, the rule will be triggered for any symbol in the system. The cool down period for alerts (frequency) is applied on a per symbol basis.
type (str): Specify either any, where if any condition is true you get an alert, or all, where all conditions must be true to trigger an alert. any is the default value
existingId (Optional[str]): The id of an existing rule only if you are editing the existing rule
conditions array Required An array of arrays. Each condition array will consist of three values; left condition, operator, right condition.
Ex: [ [‘latestPrice’, ‘>’, 200.25], [‘peRatio’, ‘<’, 20] ]
outputs array Required An array of one object. The object’s schema is defined for each notification type, and is returned by the notificationTypes array in the /rules/schema endpoint.
Every output object will contain method (which should match the value key of the notificationType, and frequency which is the number of seconds to wait between alerts.
Ex: [ { method: ‘webhook’, url: ‘https://myserver.com/iexcloud-webhook’, frequency: 60 } ]
additionalKeys array Optional. An array of schema data values to be included in alert message in addition to the data values in the conditions.
Ex: ['latestPrice', 'peRatio', 'nextEarningsDate']
'''
if type not in ('any', 'all'):
raise PyEXception('type must be in (any, all). got: {}'.format(type))

if isinstance(rule, Rule):
rule = rule.toJson()

rule['token'] = token
rule['ruleSet'] = ruleSet
rule['type'] = type
rule['ruleName'] = ruleName

# Conditions, outputs, and additionalKeys handled by rule object
if 'conditions' not in rule:
raise PyEXception('rule is missing `conditions` key!')
if 'outputs' not in rule:
raise PyEXception('rule is missing `outputs` key!')

if existingId is not None:
rule['id'] = existingId
return _postJson('rules/create', json=rule, token=token, version=version, token_in_params=False)


def pause(ruleId, token='', version=''):
'''You can control the output of rules by pausing and resume per rule id.
Args:
ruleId (str): The id of an existing rule to puase
'''
return _postJson('rules/pause', json={"ruleId": ruleId, "token": token}, token=token, version=version, token_in_params=False)


def resume(ruleId, token='', version=''):
'''You can control the output of rules by pausing and resume per rule id.
Args:
ruleId (str): The id of an existing rule to puase
'''
return _postJson('rules/resume', json={"ruleId": ruleId, "token": token}, token=token, version=version, token_in_params=False)


def delete(ruleId, token='', version=''):
'''You can delete a rule by using an __HTTP DELETE__ request. This will stop rule executions and delete the rule from your dashboard. If you only want to temporarily stop a rule, use the pause/resume functionality instead.
Args:
ruleId (str): The id of an existing rule to puase
'''
return _deleteJson('rules/{}'.format(ruleId), token=token, version=version)
Loading

0 comments on commit f18a084

Please # to comment.