Skip to content

Commit

Permalink
Add ability to initialize library with a config (#49)
Browse files Browse the repository at this point in the history
The key changes are:
* A `BaseConfig` class that defines the core functionality of the
`Config` class. This is now used for typehinting.
* `initialize_with_config` allows you to initialize the library with an
existing config instance.
* Bumping the version from 0.13.1 -> 0.14.0

This PR also includes a small fix for the CLI (a default value was
invalid; I'm assuming a later version of Click got stricter about this).
  • Loading branch information
cranti committed Mar 19, 2024
1 parent 1d95f3c commit accc336
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 14 deletions.
2 changes: 1 addition & 1 deletion cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def setup(access_token_id, access_token_secret):

@configure.command()
@click.option(
"--key", "-k", default="", multiple=True, required=False, help="1+ keys to fetch"
"--key", "-k", default=[], multiple=True, required=False, help="1+ keys to fetch"
)
def get(key):
"""
Expand Down
35 changes: 33 additions & 2 deletions runeq/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Configuration for accessing Rune APIs.
"""

import os

import boto3
Expand Down Expand Up @@ -38,9 +39,39 @@
"""


class Config:
class BaseConfig:
"""Base class to hold configuration for accessing Rune APIs."""

graph_url: str
stream_url: str

def refresh_auth(self) -> bool:
"""
Refresh authentication credentials if possible, returning a bool
indicating if the refresh occurred.
This is specific to the authentication style: e.g. it may
be implemented to refresh a JWT. By default, it is a no-op.
The API clients contain logic to catch possible authentication
errors, invoke this method, and retry the request (if credentials
are successfully refreshed).
"""
return False

@property
def auth_headers(self) -> dict:
"""
Authentication headers for HTTP requests to Rune APIs.
"""
raise NotImplementedError("auth_headers must be implemented by a subclass")


class Config(BaseConfig):
"""
Holds configuration (e.g. auth credentials, URLs, etc)
Holds configuration for Rune API usage (e.g. auth credentials, URLs, etc)
"""

Expand Down
18 changes: 13 additions & 5 deletions runeq/resources/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Clients for Rune's GraphQL API and V2 Stream API.
"""

import time
import urllib.parse
from functools import wraps
Expand All @@ -13,7 +14,7 @@
from gql.transport.requests import RequestsHTTPTransport

from runeq import errors
from runeq.config import Config
from runeq.config import BaseConfig, Config

# Error when a client is not initialized
INITIALIZATION_ERROR = errors.InitializationError(
Expand Down Expand Up @@ -66,12 +67,12 @@ class GraphClient:
"""

# Configuration details for the graph client.
config: Config = None
config: BaseConfig = None

# The GraphQL client.
_gql_client: GQLClient

def __init__(self, config: Config):
def __init__(self, config: BaseConfig):
"""
Initialize the Graph API Client.
Expand Down Expand Up @@ -131,9 +132,9 @@ class StreamClient:
HEADER_NEXT_PAGE = "X-Rune-Next-Page-Token"

# Configuration details for the stream client.
config: Config = None
config: BaseConfig = None

def __init__(self, config: Config):
def __init__(self, config: BaseConfig):
"""
Initialize the Stream API Client.
Expand Down Expand Up @@ -247,7 +248,14 @@ def initialize(*args, **kwargs):
"""
config = Config(*args, **kwargs)
initialize_with_config(config)


def initialize_with_config(config: BaseConfig):
"""Initializes the library using a config object, setting global clients
for requests to the GraphQL API and the V2 Stream API.
"""
global _graph_client, _stream_client
_graph_client = GraphClient(config)
_stream_client = StreamClient(config)
Expand Down
3 changes: 2 additions & 1 deletion runeq/stream/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Query data from the Rune Labs Stream API (V1).
"""

import csv
from logging import getLogger
from typing import Iterator, Union
Expand Down Expand Up @@ -71,7 +72,7 @@ class StreamV1Base:
# Not supported for all resources.
_availability = None

def __init__(self, cfg: config.Config, **defaults):
def __init__(self, cfg: config.BaseConfig, **defaults):
"""
Initialize with a Config and default query parameters.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name=package_name,
version="0.13.1",
version="0.14.0",
author="Rune Labs",
maintainer_email="support@runelabs.io",
description="Query data from Rune Labs APIs",
Expand Down
35 changes: 31 additions & 4 deletions tests/resources/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@
Tests for the V2 SDK Client.
"""

from unittest import TestCase, mock

from runeq import errors
from runeq.resources.client import GraphClient, StreamClient, initialize
from runeq.config import BaseConfig
from runeq.resources.client import (
GraphClient,
StreamClient,
global_graph_client,
global_stream_client,
initialize,
initialize_with_config,
)
from runeq.resources.stream import get_stream_data


Expand Down Expand Up @@ -49,6 +58,24 @@ def test_init_with_client_keys(self):
with self.assertRaisesRegex(errors.APIError, "401 InvalidAuthentication"):
get_stream_data("stream_id").__next__()

def test_initialize_with_config(self):
"""
Test initializing with a config object.
"""
config = mock.Mock(spec=BaseConfig)
config.graph_url = "graph-url"
config.stream_url = "stream-url"
config.auth_headers = {"hello": "world"}

initialize_with_config(config)

graph_client = global_graph_client()
stream_client = global_stream_client()

self.assertIs(graph_client.config, config)
self.assertIs(stream_client.config, config)


class TestStreamClient(TestCase):
"""
Expand All @@ -67,7 +94,7 @@ def _setup_mock_response(self, status_code, json_body):
@mock.patch("runeq.resources.client.requests.get")
def test_refresh_auth_on_4xx(self, mock_get):
"""If a request fails with a 4xx status code, refresh auth and retry"""
config = mock.Mock()
config = mock.Mock(spec=BaseConfig)
config.stream_url = ""
config.refresh_auth.return_value = True

Expand All @@ -85,7 +112,7 @@ def test_refresh_auth_on_4xx(self, mock_get):
@mock.patch("runeq.resources.client.requests.get")
def test_no_retry_on_5xx(self, mock_get):
"""If a request fails with a 5xx status code, do not retry"""
config = mock.Mock()
config = mock.Mock(spec=BaseConfig)
config.stream_url = ""
config.refresh_auth.return_value = True

Expand All @@ -110,7 +137,7 @@ class TestGraphClient(TestCase):
@mock.patch("runeq.resources.client.GQLClient")
def test_refresh_auth_on_error(self, mock_client_cls):
"""If a request fails with any error, refresh auth and retry"""
config = mock.Mock()
config = mock.Mock(spec=BaseConfig)
config.graph_url = ""
config.auth_headers = {"hello": "world"}

Expand Down

0 comments on commit accc336

Please sign in to comment.