Skip to content

Commit

Permalink
refactor: first attempt a dynamic discovery
Browse files Browse the repository at this point in the history
this is my first refactor towards getting dynamic discovery working
there are still issues with accessing the singlton to share the
discovered version around along side caching the discovery results

REFS #5
  • Loading branch information
devraj committed Nov 26, 2023
1 parent b46c830 commit 22752f9
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 58 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ jobs:
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
pip install poetry
poetry install
- name: Run tests
run: |
task test
47 changes: 19 additions & 28 deletions gallagher/cc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@
from typing import Optional

from ..const import URL
from ..dto.discover import DiscoveryResponse

from .core import (
APIEndpoint,
EndpointConfig
from ..dto.discover import (
DiscoveryResponse,
FeaturesDetail,
)

# Follow the instructions in the Gallagher documentation
Expand All @@ -45,27 +44,19 @@
# should you wish to use a proxy, set this to the proxy URL
proxy: Optional[str] = None


class APIFeatureDiscovery(
APIEndpoint
):
""" The Command Centre root API endpoint
Much of Gallagher's API documentation suggests that we don't
hard code the URL, but instead use the discovery endpoint by
calling the root endpoint.
This should be a singleton which is instantiated upon initialisation
and then used across the other endpoints.
For example features.events.events.href is the endpoint for the events
where as features.events.events.updates is the endpoint for getting
updates to the changes to events.
This differs per endpoint that we work with.
"""
__config__ = EndpointConfig(
endpoint="", # The root endpoint is the discovery endpoint
dto_list=DiscoveryResponse,
)
# Discover response object, each endpoint will reference
# one of the instance variable Href property to get the
# path to the endpoint.
#
# Gallagher recommends that the endpoints not be hardcoded
# into the client and instead be discovered at runtime.
#
# Note that if a feature has not been licensed by a client
# then the path will be set to None, if the client attempts
# to access the endpoint then the library will throw an exception
#
# This value is memoized and should perform
CAPABILITIES = DiscoveryResponse(
version="0.0.0", # Indicates that it's not been discovered
features=FeaturesDetail()
)
7 changes: 5 additions & 2 deletions gallagher/cc/alarms/items.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""
"""

from gallagher.cc import CAPABILITIES

from ..core import (
APIEndpoint,
EndpointConfig
EndpointConfig,
)

from ...dto.items import (
Expand All @@ -18,7 +21,7 @@ class ItemsTypes(APIEndpoint):
"""

__config__ = EndpointConfig(
endpoint=cls.paths.features.alarms.alarms.href,
endpoint="items/types",
dto_list=ItemTypesResponse,
)

Expand Down
69 changes: 44 additions & 25 deletions gallagher/cc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

import httpx

from gallagher.exception import (
UnlicensedFeatureException
)

from ..dto.discover import (
DiscoveryResponse,
FeaturesDetail,
)


Expand Down Expand Up @@ -82,6 +85,18 @@ class EndpointConfig:
sort: Optional[str] = "id" # Can be set to id or -id
# fields: list[str] = [] # Optional list of fields

@classmethod
def validate_endpoint(cls):
""" Check to see if the feature is licensed and available
Gallagher REST API is licensed per feature, if a feature is not
the endpoint is set to none and we should throw an exception
"""
if not cls.endpoint:
raise UnlicensedFeatureException(
"Endpoint not defined"
)


class APIEndpoint():
""" Base class for all API objects
Expand All @@ -94,42 +109,42 @@ class APIEndpoint():
"""

# Discover response object, each endpoint will reference
# one of the instance variable Href property to get the
# path to the endpoint.
#
# Gallagher recommends that the endpdoints not be hardcoded
# into the client and instead be discovered at runtime.
#
# Note that if a feature has not been licensed by a client
# then the path will be set to None, if the client attempts
# to access the endpoint then the library will throw an exception
#
# This value is memoized and should perform
paths = DiscoveryResponse(
version="0.0.0", # Indicates that it's not been discovered
features=FeaturesDetail()
)

# This must be overridden by each child class that inherits
# from this base class.
__config__ = None

@classmethod
def _discover(cls):
""" Discovers the endpoints for the given API
""" The Command Centre root API endpoint
Much of Gallagher's API documentation suggests that we don't
hard code the URL, but instead use the discovery endpoint by
calling the root endpoint.
This should be a singleton which is instantiated upon initialisation
and then used across the other endpoints.
For example features.events.events.href is the endpoint for the events
where as features.events.events.updates is the endpoint for getting
updates to the changes to events.
This is a memoized function that will only be called once
on the first operation that is accessed, subsequent calls
will return the cached result.
This differs per endpoint that we work with.
"""

# Auto-discovery of the API endpoints, this will
# be called as part of the bootstrapping process
from . import (
APIFeatureDiscovery
from . import api_base
response = httpx.get(
api_base,
headers=get_authorization_headers(),
)

cls.paths = APIFeatureDiscovery.list()
parsed_obj = DiscoveryResponse.model_validate(
response.json()
)

from . import CAPABILITIES
CAPABILITIES = parsed_obj

@classmethod
def list(cls, skip=0):
Expand All @@ -138,6 +153,8 @@ def list(cls, skip=0):
Most resources can be searched which is exposed by this method.
Resources also allow pagination which can be controlled by the skip
"""
cls._discover()

from . import api_base
response = httpx.get(
f'{api_base}{cls.__config__.endpoint}',
Expand All @@ -158,6 +175,8 @@ def retrieve(cls, id):
Each resource also provides a href and pagination for
children.
"""
cls._discover()

from . import api_base
response = httpx.get(
f'{api_base}{cls.__config__.endpoint}/{id}',
Expand Down
22 changes: 20 additions & 2 deletions gallagher/const.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
""" Constants for the Gallagher Cloud ecosystem
Gallagher publishes a set of networking constants for their cloud gateways
these should be updated as the documentation is updated.
"""


class URL:
""" DNS names for the cloud gateways
These are published by Gallagher on Github
https://gallaghersecurity.github.io/commandcentre-cloud-api-gateway.html
"""

CLOUD_GATEWAY_AU: str = \
"https://commandcentre-api-au.security.gallagher.cloud/api/"
CLOUD_GATEWAY_US: str = \
"https://commandcentre-api-us.security.gallagher.cloud/api/"

CLOUD_GATEWAY_AU: str = "https://commandcentre-api-au.security.gallagher.cloud/api/"
CLOUD_GATEWAY_US: str = "https://commandcentre-api-us.security.gallagher.cloud/api/"

class IP_ADDR:
""" IP addresses for the cloud gateways
These are published by Gallagher on Github
https://gallaghersecurity.github.io/commandcentre-cloud-api-gateway.html
"""

CLOUD_GATEWAY_AU = [
"3.106.1.6",
Expand Down
11 changes: 11 additions & 0 deletions gallagher/dto/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"""

from typing import (
Optional
)
from datetime import datetime

from pydantic import (
BaseModel,
Expand Down Expand Up @@ -37,6 +41,13 @@ class AppBaseModel(BaseModel):
alias_generator=to_lower_camel,
)

# Set to the last time each response was retrieved
# If it's set to None then the response was either created
# by the API client or it wasn't retrieved from the server
#
# This is generally used for caching
good_known_since: Optional[datetime] = None


class IdentityMixin(BaseModel):
""" Identifier
Expand Down
2 changes: 1 addition & 1 deletion gallagher/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""


class FeatureNotLicensedException(Exception):
class UnlicensedFeatureException(Exception):
""" Raised when a feature is not licensed
This exception is raised when the client attempts to access
Expand Down

0 comments on commit 22752f9

Please sign in to comment.