Skip to content

Open-API Specification Generation


Imports

#exports
import numpy as np
import pandas as pd

import os
import yaml

from jinja2 import Template
from IPython.display import JSON
#exports
def init_spec(
    title='BMRS API',
    description='API for the Elexon Balancing Mechanism Reporting Service',
    root_url='https://api.bmreports.com'
):
    API_spec = dict()

    API_spec['title'] = title
    API_spec['description'] = description
    API_spec['root_url'] = root_url

    return API_spec
API_spec = init_spec()

API_spec
{'title': 'BMRS API',
 'description': 'API for the Elexon Balancing Mechanism Reporting Service',
 'root_url': 'https://api.bmreports.com'}
#exports
def load_endpoints_df(endpoints_fp: str='data/endpoints.csv'):
    df_endpoints = pd.read_csv(endpoints_fp)

    date_idxs = (df_endpoints['Sample Data'].str.count('/')==2).replace(np.nan, False)
    time_idxs = df_endpoints['Sample Data'].str.contains(':').replace(np.nan, False)

    df_endpoints.loc[date_idxs & ~time_idxs, 'Sample Data'] = pd.to_datetime(df_endpoints.loc[date_idxs & ~time_idxs, 'Sample Data']).dt.strftime('%Y-%m-%d')
    df_endpoints.loc[date_idxs & time_idxs, 'Sample Data'] = pd.to_datetime(df_endpoints.loc[date_idxs & time_idxs, 'Sample Data']).dt.strftime('%Y-%m-%d %H:%M:%S')

    df_endpoints['Sample Data'] = df_endpoints['Sample Data'].fillna('')
    df_endpoints['Field Name'] = df_endpoints['Field Name'].str.replace(' ', '')

    return df_endpoints
df_endpoints = load_endpoints_df('../data/endpoints.csv')
df_endpoints.to_csv('../data/endpoints.csv', index=False)

df_endpoints.head(3)
id name version method direction Field Name Field Type Remarks Mandatory Format Sample Data
B1720 Amount Of Balancing Reserves Under Contract Se... 1 get request APIKey String nan Yes nan AP8DA23
B1720 Amount Of Balancing Reserves Under Contract Se... 1 get request SettlementDate String nan Yes YYYY-MM-DD 2021-01-01
B1720 Amount Of Balancing Reserves Under Contract Se... 1 get request Period String nan Yes */1-50 1
#exports
def get_endpoint_single_attr(df_endpoint, attribute='version'):
    attr_val = df_endpoint[attribute].unique()
    assert len(attr_val)==1, f'Expected only 1 {attribute}, instead found {len(attr_val)}'
    attr_val = attr_val[0]

    return attr_val
endpoint_id = 'B1720'
df_endpoint = df_endpoints.query(f'id==@endpoint_id')

get_endpoint_single_attr(df_endpoint, 'version')
1
#exports
def init_stream_dict(df_endpoint, endpoint_id):
    version = get_endpoint_single_attr(df_endpoint, 'version')
    name = get_endpoint_single_attr(df_endpoint, 'name')

    stream = dict()

    stream['endpoint'] = f'/BMRS/{endpoint_id}/v{version}'
    stream['description'] = name
    stream['parameters'] = list()

    return stream
stream = init_stream_dict(df_endpoint, endpoint_id)

stream
{'endpoint': '/BMRS/B1720/v1',
 'description': 'Amount Of Balancing Reserves Under Contract Service',
 'parameters': []}
#exports
def add_params_to_stream_dict(
    df_endpoint: pd.DataFrame, 
    stream: dict,
    field_type_map: dict={
        'String': 'string',
        'Int': 'integer',
        'int': 'integer',
        'Integer': 'integer',
        'Date': 'string'
    }
):
    for _, (param_name, param_type, param_sample) in df_endpoint.query('direction=="request"')[['Field Name', 'Field Type', 'Sample Data']].iterrows():               
        parameter = dict()

        parameter['name'] = param_name
        parameter['type'] = field_type_map[param_type]

        if param_type in ['Date']:
            parameter['format'] = 'date'

        if param_name == 'APIKey':
            parameter['format'] = 'password'

        if param_sample == 'csv/xml':
            parameter['examples'] = {f'{param_sub_sample}': {'value': param_sub_sample} for param_sub_sample in param_sample.split('/')}
        else:
            parameter['example'] = param_sample

        stream['parameters'] += [parameter]

    return stream
stream = add_params_to_stream_dict(df_endpoint, stream)

stream
{'endpoint': '/BMRS/B1720/v1',
 'description': 'Amount Of Balancing Reserves Under Contract Service',
 'parameters': [{'name': 'APIKey',
   'type': 'string',
   'format': 'password',
   'example': 'AP8DA23'},
  {'name': 'SettlementDate', 'type': 'string', 'example': '2021-01-01'},
  {'name': 'Period', 'type': 'string', 'example': '1'},
  {'name': 'ServiceType',
   'type': 'string',
   'examples': {'csv': {'value': 'csv'}, 'xml': {'value': 'xml'}}}]}
#exports
def add_streams_to_spec(API_spec, df_endpoints):
    API_spec['streams'] = list()
    endpoint_ids = sorted(list(df_endpoints['id'].unique()))

    for endpoint_id in endpoint_ids:
        df_endpoint = df_endpoints.query(f'id==@endpoint_id')

        stream = init_stream_dict(df_endpoint, endpoint_id)
        stream = add_params_to_stream_dict(df_endpoint, stream)

        API_spec['streams'] += [stream]

    return API_spec
API_spec = add_streams_to_spec(API_spec, df_endpoints)

JSON(API_spec)
<IPython.core.display.JSON object>
#exports
def construct_spec(
    df_endpoints: pd.DataFrame,
    title: str='BMRS API',
    description: str='API for the Elexon Balancing Mechanism Reporting Service',
    root_url: str='https://api.bmreports.com'
):
    API_spec = init_spec()
    API_spec = add_streams_to_spec(API_spec, df_endpoints)

    return API_spec
%%time

API_spec = construct_spec(df_endpoints)
Wall time: 480 ms
#exports
def save_spec(
    API_spec: dict,
    in_fp: str='../templates/open_api_spec.yaml',
    out_fp: str='../data/BMRS_API.yaml'
):
    rendered_schema = Template(open(in_fp).read()).render(API_spec=API_spec)

    with open(out_fp, 'w') as f:
        try:
            f.write(rendered_schema)
        except e as exc:
            raise exc
save_spec(API_spec)
#exports
def load_API_yaml(fp='../data/BMRS_API.yaml'):
    with open(fp, 'r') as stream:
        try:
            API_yaml = yaml.safe_load(stream)
        except yaml.YAMLError as exc:
            raise exc

    return API_yaml
API_yaml = load_API_yaml(fp='../data/BMRS_API.yaml')

JSON(API_yaml)
<IPython.core.display.JSON object>
# https://app.swaggerhub.com/apis/AyrtonB/default-title/0.1