Python API : ib1.openenergy.support¶
Python docs and source for this package
External APIs¶
- class ib1.openenergy.support.AccessTokenValidator(client_id: str, private_key: str, certificate: str, issuer_url: str = 'https://matls-auth.directory.energydata.org.uk/', client_cert_parser=None)[source]¶
Perform checks on a presented bearer token as defined in section 6.2.1 here https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#accessing-protected-resources
Uses https://tools.ietf.org/html/rfc7662 - OAuth 2.0 Token Introspection to check a supplied bearer token against an introspection endpoint for part 6.2.1.13
- __init__(client_id: str, private_key: str, certificate: str, issuer_url: str = 'https://matls-auth.directory.energydata.org.uk/', client_cert_parser=None)[source]¶
Create a new access token validator. In this context the data provider attempting to validate an access token is acting as a client to the directory’s API, so client_id, private_key and certificate are those of the data provider and not the client requesting data from it. These are not the transport keys, they’re the ones issued by the directory.
- Parameters
client_id – OAuth client ID of the data provider, used to authenticate with the introspection endpoint
private_key – Location of the private key of the data provider, used in the client auth for the introspection endpoint
certificate – Location of the public key of the data provider, used in the client auth for the introspection endpoint
issuer_url – URL of an authorization server, i.e. for the raidiam UAT sandbox this is https://matls-auth.directory.energydata.org.uk/ - uses https://openid.net/specs/openid-connect-discovery-1_0.html part 4 to discover the token introspection endpoint
client_cert_parser – A zero argument function which returns an X509 object for the active client certificate, or none if no certificate is present. Defaults to a simple implementation that pulls the cert out of the flask environment as provided by the local dev mode runner in flask_ssl_dev.py, but should be replaced when running in production mode behind e.g. nginx
- inspect_token(token: str) → Dict[source]¶
Send an access token to the introspection endpoint, returning the introspection response
- Parameters
token – access token as received as authorization in a request to the data provider’s API
- Returns
object containing parsed JSON response from the introspection endpoint
- introspects(f=None, scope=None)[source]¶
Build a decorator that can be used on flask routes to automatically introspect on any provided bearer tokens, passing the resulting object into g.introspection_response. If the introspection indicates a failed validation, the underlying route will not be called at all and an appropriate error response will be sent.
- Introspection fails if:
Querying the token introspection endpoint fails
A token is returned with active: false
Scope is specified, and the required scope is not in the token scopes
Issued time is in the future
Expiry time is in the past
Certificate binding is enabled (default) and the fingerprint of the presented client cert isn’t a match for the claim in the introspection response
If introspection succeeds, the decorated function is called and the Date and x-fapi-interaction-id headers injected into the response before returning.
- class ib1.openenergy.support.FAPISession(client_id, issuer_url, requested_scopes, private_key, certificate, jwt_bearer_email=None, signing_private_key=None, retries=3)[source]¶
Similar to a requests session, but handles management of a single access token. It acquires the token when required, manages token expiry etc. Implicitly uses client-credentials, there’s no user consent or similar involved.
Can also be used as a requests authenticator
- __init__(client_id, issuer_url, requested_scopes, private_key, certificate, jwt_bearer_email=None, signing_private_key=None, retries=3)[source]¶
Build a new FAPI session. This doesn’t immediately trigger any requests to the token endpoint, these are made when the session is accessed, and only if needed.
- Parameters
issuer_url – URL of an authorization server, i.e. for the raidiam UAT sandbox this is https://matls-auth.directory.energydata.org.uk/
requested_scopes – Scopes requested for this session
private_key – Location of private key used for MTLS, and to sign
certificate – Location of certificate used for MTLS
jwt_bearer_email – Defaults to None. If specified, this should be the email address of a user to impersonate. This will then switch the client to jwt-bearer grant type. This will only work if the corresponding client has been provisioned appropriately in the directory itself, otherwise this will fail. It should only ever be used by our internal Open Energy clients needing to write to the directory, and can be entirely ignored by other users.
signing_private_key – Default to None, must be provided if jwt_bearer_email is set. Path to private key set as a signing key in the directory.
retries – Number of retries that will be used when accessing GET endpoints through the underlying plain and FAPI enabled sessions within this object. Defaults to 3, set to 0 to disable retries. Requests which respond with statii in [429, 502, 503, 504] will be retried, exponential back-off is applied to avoid overwhelming resources.
- clear_token()[source]¶
Explicitly clear the current token, if present. This will force a refresh the next time the session is accessed.
- property introspection_response: Dict¶
Introspect on our own token, useful to see what the resource server will see when it asks about this client.
- property session¶
A requests session configured to automatically acquire tokens when needed and to use MTLS
- class ib1.openenergy.support.OpenIDConfiguration(token_endpoint: str, introspection_endpoint: str, issuer: str, scopes_supported: List[str], grant_types_supported: List[str])[source]¶
Information returned from the .well-known/openid-configuration document by an OpenID provider such as the raidiam authz service.
- __init__(token_endpoint: str, introspection_endpoint: str, issuer: str, scopes_supported: List[str], grant_types_supported: List[str]) → None¶
Initialize self. See help(type(self)) for accurate signature.
- static oidc_configuration_url(issuer_url: str)[source]¶
Implements the logic in 4.1 of https://openid.net/specs/openid-connect-discovery-1_0.html to find the URL for the configuration document which can populate an instance of OpenIDConfiguration
- Parameters
issuer_url – Base URL of the issuer
- Returns
URL of the configuration document
- class ib1.openenergy.support.RaidiamDirectory(fapi: ib1.openenergy.support.FAPISession, base_url: str = 'https://matls-dirapi.directory.energydata.org.uk/')[source]¶
Encapsulates access to the Raidiam Directory, currently just the read API. Parses responses and builds the appropriate dataclasses from the
ib1.openenergy.support.raidiam
module.- __init__(fapi: ib1.openenergy.support.FAPISession, base_url: str = 'https://matls-dirapi.directory.energydata.org.uk/')[source]¶
Initialize self. See help(type(self)) for accurate signature.
- organisations() → List[ib1.openenergy.support.raidiam.Organisation][source]¶
Get all
Organisation
entities within the directory
- ib1.openenergy.support.build(d: Dict, cls: Type[ib1.openenergy.support.D], date_format_string='%Y-%m-%dT%H:%M:%S.%fZ') → ib1.openenergy.support.D[source]¶
Build a dataclass from a dict, massaging CamelCase form into the more normal pythonic_representation, then filtering by properties available to the dataclass constructor before using the filtered set to create a new instance of the dataclass. Handles entries which are typed to other dataclasses recursively including List[OtherDataClass] types.
- Parameters
d – Dict containing properties to pass to constructor
cls – Class, typically a dataclass, to receive properties. Should be the class of the root object.
data_format_string – Format string to use when parsing dates into datetime objects, defaults to the one we’re seeing in the Raidiam directory, i.e. “%Y-%m-%dT%H:%M:%S.%fZ”
- Returns
Instance of cls configured from the supplied dict
- ib1.openenergy.support.build_error_response(error=None, code=400, scope=None, description=None, uri=None)[source]¶
RFC 6750 has a slightly odd way to complain about invalid tokens! This is used in the token authenticator class to build responses for invalid or missing tokens.
- Parameters
error – One of ‘invalid request’, ‘invalid token’, or ‘insufficient scope’
code – HTTP status code
description – Description of the error
uri – URL of the page describing the error in detail
scope – Scopes required to access the protected resource
- ib1.openenergy.support.httpclient_logging_patch(level=10)[source]¶
Enable HTTPConnection debug logging to the logging framework
- ib1.openenergy.support.nginx_cert_parser()[source]¶
A certificate parser to be used with the
AccessTokenValidator
when running behind nginx or another similar proxy which terminates SSL connections and can be configured to push the presented client certificate into a header. In this case we use the headerX-OE-CLIENT-CERT
, this function removes any errant tab characters (introduced by nginx for some reason) and parses the contents of this header as a PEM format certificate.- Returns
A parsed x509 Certificate object, or None if no cert presented in the header
Metadata APIs¶
API to handle the metadata format described in Data Set Metadata
- ib1.openenergy.support.metadata.DC = 'http://purl.org/dc/terms/'¶
Dublin Core namespace
- ib1.openenergy.support.metadata.DCAT = 'http://www.w3.org/ns/dcat#'¶
Data Catalogue namespace
- class ib1.openenergy.support.metadata.JSONLDContainer(d: Dict)[source]¶
Wraps up the data structure returned by jsonld.expand and adds some convenience methods to query properties within it
- get(namespace: str, term: str, default=None)[source]¶
Get a property, handles looking for the @value entries within an expanded JSON-LD dictionary
- Parameters
namespace – namespace for term to find
term – term within that namespace
default – default value to return if term isn’t present, defaults to None
- Returns
value of term, can be single item if only one value present or list if multiple
- require_values(d: Dict[str, List[str]])[source]¶
Require that this container has the specified values, defined as a dict of namespace to list of terms.
- Parameters
d – Dict of str namespace to list of str terms that must be present
- Raises
ValueError if any specified values are not present in this container
- property type¶
@type
of the entity described
- class ib1.openenergy.support.metadata.Metadata(d: Dict)[source]¶
Representation of the information held in a data set metadata file, as defined in https://icebreakerone.github.io/open-energy-technical-docs/main/metadata.html - this implementation will track the spec, but may not be complete, we’ll build capabilities into it as and when we need them.
Currently models the content part of the metadata file as a
JSONLDContainer
- __init__(d: Dict)[source]¶
Create a new metadata container. Currently just parses the
content
section of the metadata file.- Parameters
d – a dict containing the four top level keys from the metadata spec
- Raises
ValueError – if the structure of the dict is invalid in some way
- property data_sensitivity_class: str¶
content / oe:sensitivityClass [OE-O|OE-SA|OE-SB]
- property description¶
content / dc:description
- property keywords: List[str]¶
content / dcat:keywords
- property stable_identifier: str¶
content / oe:dataSetStableIdentifier
- property title: str¶
content / dc:title
- property version¶
content / dcat:version
- property version_notes¶
content / dcat:versionNotes
- class ib1.openenergy.support.metadata.MetadataLoadResult(location: Optional[str] = None, error: Optional[str] = None, exception: Optional[Exception] = None, metadata: List[ib1.openenergy.support.metadata.Metadata] = <factory>, server: Optional[ib1.openenergy.support.raidiam.AuthorisationServer] = None)[source]¶
Information about the process of loading a metadata file from a URL or file location along with the results. This is used instead of raising exceptions during the load process in order to provide better reporting with mappings between org IDs and problems with their respective metadata files.
- __init__(location: Optional[str] = None, error: Optional[str] = None, exception: Optional[Exception] = None, metadata: List[ib1.openenergy.support.metadata.Metadata] = <factory>, server: Optional[ib1.openenergy.support.raidiam.AuthorisationServer] = None) → None¶
Initialize self. See help(type(self)) for accurate signature.
- ib1.openenergy.support.metadata.OE = 'http://energydata.org.uk/oe/terms/'¶
Open Energy ontology namespace
- ib1.openenergy.support.metadata.load_metadata(server: Optional[ib1.openenergy.support.raidiam.AuthorisationServer] = None, url: Optional[str] = None, file: Optional[str] = None, session=None, convert_tabs_to_spaces=True, **kwargs) → ib1.openenergy.support.metadata.MetadataLoadResult[source]¶
Load metadata from a URL.
- Parameters
server –
AuthorisationServer
from which this url was retrieved, or None if fetching directlyurl – url from which to load metadata
file – file path from which to load metadata, use either this or url, not both
session – if specified, use this
requests.Session
, if not, create a new oneconvert_tabs_to_spaces – normally YAML is invalid if it uses tab characters as indentation. If this argument is set to true, a second attempt will be made to parse the file if a scanner error occurs, first doing a global search and replace to change all tab characters to double spaces. Defaults to False, as this isn’t really ‘allowed’ according to the spec.
kwargs – any additional arguments to pass into the get request
- Returns
a
MetadataLoadResult
containing a report on the process, including actualMetadata
objects if the load succeeded and found any metadata.
- ib1.openenergy.support.metadata.load_yaml_from_bytes(b: bytes, convert_tabs_to_spaces=True)[source]¶
Attempt to load YAML from a set of bytes.
- Parameters
b – bytes to load
convert_tabs_to_spaces – if True (default), an initial failure to load YAML will be retried after converting all tab characters in the input to double spaces. This is done after converting the bytes to a string with UTF8 encoding, and after the tabs are stripped the string is encoded back to UTF8 bytes before passing back to the yaml loader
- Returns
yaml parsed as a dict
- Raises
YAMLError if unable to parse the input bytes
Gunicorn support APIs¶
Support for running data providers within the Gunicorn WSGI container
Support for gunicorn with client certificates, with much help from a blog at https://eugene.kovalev.systems/blog/flask_client_auth
- ib1.openenergy.support.gunicorn.CERT_NAME = 'X-OE-CLIENT-CERT'¶
Name for the header containing the client certificate as a BASE64 encoded DER file
- class ib1.openenergy.support.gunicorn.ClientAuthApplication(app, cert_path, key_path, hostname='localhost', port='443', num_workers=4, timeout=30)[source]¶
GUnicorn application using the custom SSL worker. Uses certifi for its CA store. This is a helper class, mostly useful when you need to run a data provider as part of a unit test, all it really does is remove the need for a gunicorn.conf.py configuration file. See this blog for more details on how to run a data provider within a test context using this class.
- __init__(app, cert_path, key_path, hostname='localhost', port='443', num_workers=4, timeout=30)[source]¶
Create a new application runner
- Parameters
app – WSGP app to run
cert_path – Path to the server certificate
key_path – Path to the server private key
hostname – Hostname, defaults to ‘localhost’
port – Port, defaults to 443
num_workers – Number of concurrent workers, defaults to 4
timeout – Timeout, defaults to 30
- class ib1.openenergy.support.gunicorn.CustomSyncWorker(age, ppid, sockets, app, timeout, cfg, log)[source]¶
Push x509 certificate from SSL context into the named header in BASE64 encoded format. Uses the header name defined as
CERT_NAME
- ib1.openenergy.support.gunicorn.LOG = <Logger ib1.openenergy.support.gunicorn (WARNING)>¶
Log to ib1.openenergy.support.gunicorn
- ib1.openenergy.support.gunicorn.gunicorn_cert_parser() → cryptography.x509.base.Certificate[source]¶
Pull x509 client cert out of the header used by the sync worker defined in this package. Header contains BASE64 encoded DER format certificate.
Use this as the client_cert_parser argument to
AccessTokenValidator
to allow it to pull certificates out of the named header.
SSL Development APIs¶
Warning
This is deprecated since 0.2.4, use the gunicorn support above instead.
- class ib1.openenergy.support.flask_ssl_dev.SSLOptions(server_private_key: str = None, server_certificate: str = None, client_private_key: str = None, client_certificate: str = None, client_id: str = None)[source]¶
- ib1.openenergy.support.flask_ssl_dev.get_command_line_ssl_args(default_server_private_key='./a.key', default_server_certificate='./a.pem', default_client_private_key='./b.key', default_client_certificate='./b.pem', default_client_id='CLIENT_ID') → ib1.openenergy.support.flask_ssl_dev.SSLOptions[source]¶
Parse command line arguments for SSL file paths and private key password (optional)
- Parameters
default_server_certificate – default location for app certificate, ‘./app.crt’. This is the certificate the web server is going to use to create an HTTPS endpoint, it is not the one from the directory!
default_server_private_key – default location for app private key, ‘./app.key’. This is the private key of the app certificate, and not the one you used when creating the raidiam cert.
default_client_certificate – default location for the client certificate
default_client_private_key – default location for the client private key
default_client_id – default OAuth2 client ID
- Returns
parsed options object
- Raises
ValueError – if any files are not specified, or not found
- ib1.openenergy.support.flask_ssl_dev.run_app(app, *args, server_private_key, server_certificate, **kwargs)[source]¶
Run the provided app object, parsing command line arguments to get the various SSL parameters needed for client cert validation. Checks for existence of SSL related file paths, then runs the supplied flask app. This shouldn’t be used in production, but is a quick way to spin up an app with HTTPS and client auth enabled. Any routes in the resultant app will have access to a client cert (if present) through request.environ[“peercert”].
In any real case we’d be running flask behind something like nginx, in which case client certificates are handled first by the proxy, and then generally pushed through as an additional header. This mechanism wouldn’t work in those cases, it really is just for development mode and any access to the client certs should allow for both possible mechanisms so that code works in both dev and production contexts.
- Parameters
app – flask app to run with client certificate support
args – any additional positional arguments to pass to app.run
server_private_key – location of private key file for app SSL
server_certificate – app SSL certificate
kwargs – any keyword arguments to pass to app.run, ssl_context and request_handler are already set up by this function
Internal APIs¶
Note
The classes below are primarily used internally within Open Energy to manage information in our membership directory and CKAN servers, they are unlikely to be of interest to third parties implementing Data Provider or Consumer components.
- class ib1.openenergy.support.raidiam.ApiDiscoveryEndpoint(api_discovery_id: str, api_endpoint: str)[source]¶
- class ib1.openenergy.support.raidiam.ApiResource(api_resource_id: str, api_family_type: str, api_version: str, api_discovery_endpoints: List[ib1.openenergy.support.raidiam.ApiDiscoveryEndpoint])[source]¶
- class ib1.openenergy.support.raidiam.AuthorisationServer(authorisation_server_id: str, organisation_id: str, auto_registration_supported: bool = False, customer_friendly_description: str = '', customer_friendly_logo_uri: str = '', customer_friendly_name: str = '', developer_portal_uri: str = '', terms_of_service_uri: str = '', open_i_d_discovery_document: str = '', payload_signing_cert_location_uri: str = '', parent_authorisation_server_id: str = '', api_resources: List[ib1.openenergy.support.raidiam.ApiResource] = None, notification_webhook: str = '', notification_webhook_status: str = '')[source]¶
- __init__(authorisation_server_id: str, organisation_id: str, auto_registration_supported: bool = False, customer_friendly_description: str = '', customer_friendly_logo_uri: str = '', customer_friendly_name: str = '', developer_portal_uri: str = '', terms_of_service_uri: str = '', open_i_d_discovery_document: str = '', payload_signing_cert_location_uri: str = '', parent_authorisation_server_id: str = '', api_resources: Optional[List[ib1.openenergy.support.raidiam.ApiResource]] = None, notification_webhook: str = '', notification_webhook_status: str = '') → None¶
Initialize self. See help(type(self)) for accurate signature.
- class ib1.openenergy.support.raidiam.Organisation(organisation_id: str, status: str, organisation_name: str, created_on: datetime.datetime, legal_entity_name: str, country_of_registration: str, company_register: str, registration_number: str, registered_name: str, city: str, postcode: str, country: str, requires_participant_terms_and_conditions_signing: bool, registration_id: str = '', address_line1: str = '', address_line2: str = '', parent_organisation_reference: str = '')[source]¶
- __init__(organisation_id: str, status: str, organisation_name: str, created_on: datetime.datetime, legal_entity_name: str, country_of_registration: str, company_register: str, registration_number: str, registered_name: str, city: str, postcode: str, country: str, requires_participant_terms_and_conditions_signing: bool, registration_id: str = '', address_line1: str = '', address_line2: str = '', parent_organisation_reference: str = '') → None¶
Initialize self. See help(type(self)) for accurate signature.
- class ib1.openenergy.support.raidiam.OrganisationAuthorityDomainClaim(organisation_authority_domain_claim_id: str, authorisation_domain_name: str, authority_id: str, authority_name: str, registration_id: str, status: str)[source]¶
- class ib1.openenergy.support.raidiam.OrganisationContact(contact_id: str, organisation_id: str, contact_type: dict, first_name: str, last_name: str, department: str, email_address: str, phone_number: str, address_line1: str, address_line2: str, city: str, postcode: str, country: str, additional_information: str, pgp_public_key: str)[source]¶
- __init__(contact_id: str, organisation_id: str, contact_type: dict, first_name: str, last_name: str, department: str, email_address: str, phone_number: str, address_line1: str, address_line2: str, city: str, postcode: str, country: str, additional_information: str, pgp_public_key: str) → None¶
Initialize self. See help(type(self)) for accurate signature.
Support for creating and updating records in a remote CKAN instance from various organisation and metadata types from elsewhere in this library.
- ib1.openenergy.support.ckan.ckan_dataset_name(org: ib1.openenergy.support.raidiam.Organisation, data_set: ib1.openenergy.support.metadata.Metadata) → str[source]¶
Calculates a stable dataset ID for a given organisation and metadata object, uses the literal ‘oe’, and the ckan_legal_name forms of the organisation ID and data set stable identifier separated by ‘-‘ characters
- ib1.openenergy.support.ckan.ckan_dict_from_metadata(m: ib1.openenergy.support.metadata.Metadata) → dict[source]¶
Create a CKAN dict suitable for data package create or update operations from a
Metadata
object. Currently handles title, notes, version, tags, and adds oe:dataSensitivityClass and dcat:versionNotes to the extras dict. Tags are added without any associated vocabulary at this point.- Parameters
m – a
Metadata
object containing information about the data set to store or update- Returns
a dict suitable for CKAN update / create operations on data packages
- ib1.openenergy.support.ckan.ckan_legal_name(s: str) → str[source]¶
CKAN names must be between 2 and 100 characters long and contain only lowercase alphanumeric characters, ‘-‘ and ‘_’. This function makes a reasonable best effort to convert an arbitrary input string. Strings below 2 characters long will raise a ValueError.
Longer strings are converted to lower case, stripped of all non-alphanumeric, non underscore or dash, characters, then truncated to 100 characters if necessary.
- Parameters
s – input string
- Returns
valid CKAN name, as close as possible to the original
- Raises
ValueError – if the string is too short
- ib1.openenergy.support.ckan.update_or_create_ckan_record(org: ib1.openenergy.support.raidiam.Organisation, data_sets: List[ib1.openenergy.support.metadata.Metadata], ckan_api_key: str, ckan_url: str) → List[Dict][source]¶
Create or update records for a given datasets, each defined by a
Metadata
object, in the context of anOrganisation
from the directory. The organisation will be created if required.- Parameters
org –
Organisation
to use as owner of this data setdata_sets – list of
Metadata
containing information about the data setsckan_api_key – api key to write to CKAN
ckan_url – url of the CKAN instance
- Returns
list of created or modified record from CKAN as dicts
- Raises
NotAuthorized – if the supplied access token doesn’t have necessary permissions
Command line tools used to monitor the contents of the directory, largely for internal Icebreaker use.
- ib1.openenergy.support.directory_tools.get_directory_client(parser=None) → ib1.openenergy.support.RaidiamDirectory[source]¶
Parse arguments and get a directory client
- Parameters
parser – Existing parser to use, None by default will create a new one
- Returns
A
RaidiamDirectory
client