diff --git a/CHANGELOG.md b/CHANGELOG.md index f64dd90..b863b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v2.5.2 + +#### Enhancements + +- Send new request headers for metrics and troubleshooting with the `set_context` + method on the `analyzer` module and within the core API request libs. +- Abstract package version into a distinct file to consolidate updates and ensure + consistency across docs and pypi. Add `get_version` method to `analyzer` module + for easy access to the current version number. + + +#### Bug Fixes + + + + ## v2.5.1 #### Enhancements diff --git a/docs/conf.py b/docs/conf.py index a54f55a..d3d03a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,7 @@ import sys import os +import re # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -55,14 +56,12 @@ copyright = u'2021, RiskIQ' author = u'RiskIQ' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '2.5' -# The full version, including alpha/beta/rc tags. -release = '2.5.1' +# pylint: disable=locally-disabled, invalid-name +with open('../passivetotal/_version.py', 'r') as fd: + v_match = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE) + release = v_match.group(1) + version = '.'.join(release.split('.')[0:-1]) +# pylint: enable=locally-disabled, invalid-name # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/passivetotal/_version.py b/passivetotal/_version.py new file mode 100644 index 0000000..613be43 --- /dev/null +++ b/passivetotal/_version.py @@ -0,0 +1 @@ +VERSION="2.5.2" \ No newline at end of file diff --git a/passivetotal/analyzer/__init__.py b/passivetotal/analyzer/__init__.py index 35557bc..77b7536 100644 --- a/passivetotal/analyzer/__init__.py +++ b/passivetotal/analyzer/__init__.py @@ -3,6 +3,8 @@ from collections import namedtuple from datetime import datetime, timezone, timedelta from passivetotal import * +from passivetotal._version import VERSION +from passivetotal.api import Context from passivetotal.analyzer._common import AnalyzerError, AnalyzerAPIError, is_ip DEFAULT_DAYS_BACK = 90 @@ -56,6 +58,7 @@ def init(**kwargs): else: api_clients[name] = c.from_config() api_clients[name].exception_class = AnalyzerAPIError + api_clients[name].set_context('python','passivetotal',VERSION,'analyzer') config['is_ready'] = True def get_api(name): @@ -91,6 +94,23 @@ def get_object(input, type=None): raise AnalyzerError('type must be IPAddress or Hostname') return objs[type](input) +def get_version(): + """Get the current version of this package.""" + return VERSION + +def set_context(provider, variant, version, feature=''): + """Define the application context for an implementation using the analyzer module. + + Sets a header to be sent in API requests that is used for metrics and troubleshooting. + + :param provider: The company, partner, provider or other top-level application context. + :param variant: The specific app, libary subcomponent, or feature category. + :param version: Version of the app, feature or code setting the context. + :param feature: Optional sub-feature, dashboard or script name. + """ + for client in api_clients.values(): + client.set_context(provider, variant, version, feature) + def set_date_range(days_back=DEFAULT_DAYS_BACK, start=None, start_date=None, end=None, end_date=None): """Set a range of dates for all date-bounded API queries. diff --git a/passivetotal/api.py b/passivetotal/api.py index 0993f7f..4ed7748 100644 --- a/passivetotal/api.py +++ b/passivetotal/api.py @@ -1,13 +1,17 @@ """PassiveTotal API Interface.""" -__author__ = 'Brandon Dixon (PassiveTotal)' -__version__ = '1.0.0' import json import logging import requests import sys +from urllib.parse import quote as urlquote from passivetotal.config import Config +from passivetotal._version import VERSION + +__author__ = 'Brandon Dixon (PassiveTotal)' +__version__ = VERSION + class Client(object): @@ -58,6 +62,7 @@ def __init__(self, username, api_key, server=DEFAULT_SERVER, if '127.0.0.1' in server: self.verify = False self.exception_class = exception_class + self.set_context('python','passivetotal',VERSION) @classmethod def from_config(cls): @@ -78,6 +83,18 @@ def set_debug(self, status): self.logger.setLevel('DEBUG') else: self.logger.setLevel('INFO') + + def set_context(self, provider, variant, version, feature=''): + """Set the context for this request. + + :param provider: The company, partner, provider or other top-level application context. + :param variant: The specific app, libary subcomponent, or feature category. + :param version: Version of the app, feature or code setting the context. + :param feature: Optional sub-feature, dashboard or script name. + """ + context = Context(provider, variant, version, feature) + self.context = context + self.headers.update(context.get_header()) def _endpoint(self, endpoint, action, *url_args): """Return the URL for the action. @@ -181,3 +198,30 @@ def _send_data(self, method, endpoint, action, kwargs['proxies'] = self.proxies response = requests.request(method, api_url, **kwargs) return self._json(response) + + + +class Context: + + """Integration context for a set of API requests.""" + + HEADER_NAME = 'X-RISKIQ-CONTEXT' + + def __init__(self, provider, variant, version, feature = ''): + """Build a new context header. + + :param provider: The company, partner, provider or other top-level application context. + :param variant: The specific app, libary subcomponent, or feature category. + :param version: Version of the app, feature or code setting the context. + :param feature: Optional sub-feature, dashboard or script name. + """ + self._fields = (provider, variant, version, feature) + + def get_header_name(self): + return self.HEADER_NAME + + def get_header_value(self): + return '/'.join(map(lambda f: urlquote(f, safe=''), self._fields)) + + def get_header(self): + return { self.get_header_name() : self.get_header_value() } \ No newline at end of file diff --git a/setup.py b/setup.py index 27d1ac7..0f0a469 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,21 @@ #!/usr/bin/env python import os +import re from setuptools import setup, find_packages def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() +# pylint: disable=locally-disabled, invalid-name +with open('passivetotal/_version.py', 'r') as fd: + v_match = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE) + __version__ = v_match.group(1) if v_match else 'no version' +# pylint: enable=locally-disabled, invalid-name + setup( name='passivetotal', - version='2.5.1', + version=__version__, description='Library for the RiskIQ PassiveTotal and Illuminate API', url="https://github.com/passivetotal/python_api", author="RiskIQ",