diff --git a/README.md b/README.md index c01a966..a2d1c50 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,7 @@ Currently, it is working, but it's still missing many crucial features, such as: - Testing - Support for the following core services - Eventhandler - - Gateway - - Gatekeeper - - Support for the following security modes (access policies): - - Token security - - Insecure security + - Support for the TOKEN security modes (access policy): As more Arrowhead Core Systems mature and are added to the official docker container, those will be added to this list. @@ -34,65 +30,3 @@ A guide on how to create your own certificates can be found on the [Arrowhead gi ## How To Use Install the library with `pip install arrowhead-client`. -### Providing Services -To provide services, import the `ProviderSystem`. -Services can be added to the provider with the `provided_services` decorator. -The `provided_services` decorator will create a service, and register the service with the provider. -Then when the provider is started, using the `run_forever` method, the provider will automatically register the service with the service registry. - -#### Code Example -```python -import datetime -from arrowhead_client.system.provider import ProviderSystem - -# Create provider -time_provider = TimeProvider( - 'time_provider', - 'localhost', - 1337, - '', - keyfile='certificates/time_provider.key' - certfile='certificates/time_provider.crt') - -# Add service -@time_provider.provided_service('echo', '/time/echo', 'HTTP-SECURE-JSON', 'GET') -def echo(): - return {'now': str(datetime.datetime.now())} - -if __name__ == '__main__': - time_provider.run_forever() - -``` - -### Consuming Services -To consume services, use the `ConsumerSystem`. -To find a consumed service, you first register a service definition using the `add_consumed_services` method. -When the `ConsumerSystem` is started initialized, it queries the orchestrator and will register the first orchestrated service. -That service is then consumed using the `consume_service` method. - -The orchestration query _will fail_ if the orchestrator does not return a service, and crash as a consequence. -The plan is to make this robust later. - -#### Code Examples -```python - -from arrowhead_client.system.consumer import ConsumerSystem - -time_consumer = ConsumerSystem( - 'consumer_test', - 'localhost', - '1338', - '', - 'certificates/consumer_test.key', - 'certificates/consumer_test.crt') - -# Add orchestration rules -time_consumer.add_orchestration_rule('echo', 'GET') - -if __name__ == '__main__': - # Consume service provided by the 'get_time' rule - echo = time_consumer.consume_service('echo') - print(echo['echo']) - -``` - diff --git a/_version.py b/_version.py new file mode 100644 index 0000000..f82e37f --- /dev/null +++ b/_version.py @@ -0,0 +1,4 @@ +__lib_name__ = 'arrowhead-client' +__version__ = '0.2.0a2' +__author__ = 'Jacob Nilsson' +__email__ = 'jacob.nilsson@ltu.se' \ No newline at end of file diff --git a/arrowhead_client/abc.py b/arrowhead_client/abc.py new file mode 100644 index 0000000..08e8176 --- /dev/null +++ b/arrowhead_client/abc.py @@ -0,0 +1,40 @@ +from abc import abstractmethod +from typing import Any, Callable +try: + from typing import Protocol +except ImportError: + from typing_extensions import Protocol # type: ignore + + +class BaseConsumer(Protocol): + @abstractmethod + def consume_service( + self, + service_uri: str, + method: str, + **kwargs) -> Any: # type: ignore + raise NotImplementedError + + @abstractmethod + def extract_payload( + self, + service_response: Any, + payload_type: str): + raise NotImplementedError + + +class BaseProvider(Protocol): + @abstractmethod + def add_provided_service( + self, + service_definition: str, + service_uri: str, + method: str, + func: Callable, + *func_args, + **func_kwargs, ) -> None: + pass + + @abstractmethod + def run_forever(self) -> None: + pass diff --git a/arrowhead_client/api.py b/arrowhead_client/api.py index 0492345..fd32b10 100644 --- a/arrowhead_client/api.py +++ b/arrowhead_client/api.py @@ -1,13 +1,33 @@ +""" +Arrowhead Client API module +=========================== + +This module contains the api of the :code:`arrowhead_client` module. +""" from arrowhead_client.configuration import config -from arrowhead_client.application import ArrowheadApplication +from arrowhead_client.client import ArrowheadClient from arrowhead_client.system import ArrowheadSystem -from arrowhead_client.consumer import Consumer -from arrowhead_client.provider import Provider +from arrowhead_client.httpconsumer import HttpConsumer +from arrowhead_client.httpprovider import HttpProvider +from arrowhead_client.service import Service # noqa: F401 from arrowhead_client.logs import get_logger +from gevent import pywsgi # type: ignore + + +class ArrowheadHttpClient(ArrowheadClient): + """ + Arrowhead client using HTTP. + Args: + system_name: A string to assign the system name + address: A string to assign the system address + port: An int to assign the system port + authentication_info: A string to assign the system authentication info + keyfile: A string to assign the PEM keyfile + certfile: A string to assign the PEM certfile + """ -class ArrowheadHttpApplication(ArrowheadApplication): def __init__(self, system_name: str, address: str, @@ -15,14 +35,22 @@ def __init__(self, authentication_info: str = '', keyfile: str = '', certfile: str = ''): + logger = get_logger(system_name, 'debug') + wsgi_server = pywsgi.WSGIServer( + (address, port), + None, + keyfile=keyfile, + certfile=certfile, + log=logger, + ) super().__init__( ArrowheadSystem(system_name, address, port, authentication_info), - Consumer(keyfile, certfile), - Provider(), - get_logger(system_name, 'debug'), + HttpConsumer(), + HttpProvider(wsgi_server), + logger, config, keyfile=keyfile, certfile=certfile ) self._logger.info(f'{self.__class__.__name__} initialized at {self.system.address}:{self.system.port}') - #TODO: This line is a hack and needs to be fixed + # TODO: This line is a hack and needs to be fixed diff --git a/arrowhead_client/application.py b/arrowhead_client/application.py deleted file mode 100644 index 10a22ca..0000000 --- a/arrowhead_client/application.py +++ /dev/null @@ -1,224 +0,0 @@ -from __future__ import annotations -from typing import Any, Dict, Tuple -from gevent import pywsgi # type: ignore -from arrowhead_client.consumer import Consumer -from arrowhead_client.provider import Provider -from arrowhead_client.service import Service -from arrowhead_client.core_services import core_service -from arrowhead_client.system import ArrowheadSystem -from arrowhead_client import core_service_forms as forms -from arrowhead_client import core_service_responses as responses - - -class ArrowheadApplication(): - def __init__(self, - system: ArrowheadSystem, - consumer: Consumer, - provider: Provider, - logger: Any, - config: Dict, - server: Any = None, # type: ignore - keyfile: str = '', - certfile: str = '', ): - self.system = system - self.consumer = consumer - self.provider = provider - self._logger = logger - self.keyfile = keyfile - self.certfile = certfile - self.config = config - #TODO: Remove this hardcodedness - self.server = pywsgi.WSGIServer((self.system.address, self.system.port), self.provider.app, - keyfile=self.keyfile, certfile=self.certfile, - log=self._logger) - self._core_system_setup() - self.add_provided_service = self.provider.add_provided_service - - @property - def cert(self) -> Tuple[str, str]: - return self.certfile, self.keyfile - - ''' - @classmethod - def from_cfg(cls, properties_file: str) -> ArrowheadHttpApplication: - """ Creates a BaseArrowheadSystem from a descriptor file """ - - # Parse configuration file - config = configparser.ConfigParser() - with open(properties_file, 'r') as properties: - config.read_file(properties) - config_dict = {k: v for k, v in config.items('SYSTEM')} - - # Create class instance - system = cls(**config_dict) - - return system - ''' - - def _core_system_setup(self) -> None: - service_registry = ArrowheadSystem( - 'service_registry', - str(self.config['service_registry']['address']), - int(self.config['service_registry']['port']), - '' - ) - orchestrator = ArrowheadSystem( - 'orchestrator', - str(self.config['orchestrator']['address']), - int(self.config['orchestrator']['port']), - '') - - self._store_consumed_service(core_service('register'), service_registry, 'POST') - self._store_consumed_service(core_service('unregister'), service_registry, 'DELETE') - self._store_consumed_service(core_service('orchestration-service'), orchestrator, 'POST') - - def consume_service(self, service_definition: str, **kwargs): - return self.consumer.consume_service(service_definition, **kwargs) - - def add_consumed_service(self, - service_definition: str, - http_method: str) -> None: - """ Add orchestration rule for service definition """ - - orchestration_form = forms.OrchestrationForm(self.system.dto, service_definition) - - orchestration_response = self.consume_service('orchestration-service', - json=orchestration_form.dto, - cert=self.cert, - ) - #TODO: Handle orchestrator error codes - - orchestration_payload = orchestration_response.json() # This might change with backend - - (orchestrated_service, system), *_ = responses.handle_orchestration_response(orchestration_payload) - - #TODO: Handle response with more than 1 service - # Perhaps a list of consumed services for each service definition should be stored - self._store_consumed_service(orchestrated_service, system, http_method) - - def _store_consumed_service(self, - service: Service, - system: ArrowheadSystem, - http_method: str) -> None: - """ Register consumed services with the consumer """ - - self.consumer._consumed_services[service.service_definition] = (service, system, http_method) - - def provided_service(self, - service_definition: str, - service_uri: str, - interface: str, - method: str): - def wrapped_func(func): - self.provider.add_provided_service( - service_definition, - service_uri, - interface, - http_method=method, - view_func=func) - return func - return wrapped_func - - def _register_service(self, service: Service): - """ Registers the given service with service registry """ - - service_registration_form = forms.ServiceRegistrationForm( - service_definition=service.service_definition, - service_uri=service.service_uri, - secure='CERTIFICATE', - # TODO: secure should _NOT_ be hardcoded - interfaces=service.interface.dto, - provider_system=self.system.dto - ) - - service_registration_response = self.consume_service( - 'register', - json=service_registration_form.dto, - ) - - print(service_registration_response.status_code) - # TODO: Error handling - - # TODO: Do logging - - - def _register_all_services(self) -> None: - """ Registers all services of the system. """ - for service, _ in self.provider.provided_services.values(): - self._register_service(service) - - - def _unregister_service(self, service_definition: str) -> None: - """ Unregisters the given service with the service registry. """ - - if service_definition not in self.provider.provided_services.keys(): - raise ValueError(f'{service_definition} not provided by {self}') - - unregistration_payload = { - 'service_definition': service_definition, - 'system_name': self.system.system_name, - 'address': self.system.address, - 'port': self.system.port - } - - service_unregistration_response = self.consume_service( - 'unregister', - params=unregistration_payload - ) - - print(service_unregistration_response.status_code) - - - def _unregister_all_services(self) -> None: - """ Unregisters all services of the system """ - - for service_definition in self.provider.provided_services: - self._unregister_service(service_definition) - - - def run_forever(self) -> None: - """ Start the server, publish all service, and run until interrupted. Then, unregister all services""" - - import warnings - warnings.simplefilter('ignore') - - self._register_all_services() - try: - self._logger.info(f'Starting server') - print('Started Arrowhead System') - self.server.serve_forever() - except KeyboardInterrupt: - self._logger.info(f'Shutting down server') - print('Shutting down Arrowhead system') - self._unregister_all_services() - finally: - self._logger.info(f'Server shut down') - - - """ - def __enter__(self): - '''Start server and register all services''' - import warnings - warnings.simplefilter('ignore') - - print('Starting server') - self.server.start() - print('Registering services') - self.register_all_services() - - def __exit__(self, exc_type, exc_value, tb): - '''Unregister all services and stop the server''' - if exc_type != KeyboardInterrupt: - print(f'Exception was raised:') - print(exc_value) - - print('\nSystem was stopped, unregistering services') - self.unregister_all_services() - print('Stopping server') - self.server.stop() - print('Shutdown completed') - - return True - """ -if __name__ == '__main__': - pass diff --git a/arrowhead_client/client/__init__.py b/arrowhead_client/client/__init__.py new file mode 100644 index 0000000..029e082 --- /dev/null +++ b/arrowhead_client/client/__init__.py @@ -0,0 +1,3 @@ +from .client_core import ArrowheadClient + +__all__ = ['ArrowheadClient'] diff --git a/arrowhead_client/client/client_core.py b/arrowhead_client/client/client_core.py new file mode 100644 index 0000000..884d315 --- /dev/null +++ b/arrowhead_client/client/client_core.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +from typing import Any, Dict, Tuple, Callable, Union +from arrowhead_client.system import ArrowheadSystem +from arrowhead_client.abc import BaseConsumer, BaseProvider +from arrowhead_client.service import Service +from arrowhead_client.client.core_services import core_service +from arrowhead_client.client import core_service_forms as forms, core_service_responses as responses + +StoredConsumedService = Dict[str, Tuple[Service, ArrowheadSystem, str]] +StoredProvidedService = Dict[str, Tuple[Service, Callable]] + + +class ArrowheadClient(): + """ + Application class for Arrowhead Systems. + + This class serves as a bridge that connects systems, consumers, and providers to the user. + + Args: + system: ArrowheadSystem + consumer: Consumer + provider: Provider + logger: Logger, will default to the logger found in logs.get_logger() + config: JSON config file containing the addresses and ports of the core systems + server: WSGI server + keyfile: PEM keyfile + certfile: PEM certfile + """ + + def __init__(self, + system: ArrowheadSystem, + consumer: BaseConsumer, + provider: BaseProvider, + logger: Any, + config: Dict, + keyfile: str = '', + certfile: str = '', ): + self.system = system + self.consumer = consumer + self.provider = provider + self._logger = logger + self.keyfile = keyfile + self.certfile = certfile + self.secure = True if self.keyfile else False + self.config = config + self._consumed_services: StoredConsumedService = {} + self._provided_services: StoredProvidedService = {} + + # Setup methods + self._core_system_setup() + self.add_provided_service = self.provider.add_provided_service + + def consume_service(self, service_definition: str, **kwargs): + """ + Consumes the given service definition + + Args: + service_definition: The service definition of a consumable service + **kwargs: Collection of keyword arguments passed to the consumer. + """ + consumed_service, consumer_system, method = self._consumed_services[service_definition] + + service_uri = _service_uri(consumed_service, consumer_system) + + if consumed_service.interface.secure == 'SECURE': + # Add certificate files if service is secure + kwargs['cert'] = self.cert + + return self.consumer.consume_service(service_uri, method, **kwargs) + + def extract_payload(self, service_response: Any, payload_type: str) -> Union[Dict, str]: + return self.consumer.extract_payload(service_response, payload_type) + + def add_consumed_service(self, + service_definition: str, + method: str, + **kwargs, ) -> None: + """ + Add orchestration rule for service definition + + Args: + service_definition: Service definition that is looked up from the orchestrator. + method: The HTTP method given in uppercase that is used to consume the service. + """ + + orchestration_form = forms.OrchestrationForm( + self.system.dto, + service_definition, + **kwargs + ) + + orchestration_response = self.consume_service( + 'orchestration-service', + json=orchestration_form.dto, + cert=self.cert, + ) + + # TODO: Handle orchestrator error codes + + orchestration_payload = self.consumer.extract_payload( + orchestration_response, + 'json' + ) + + (orchestrated_service, system), *_ = responses.handle_orchestration_response(orchestration_payload) + + # TODO: Handle response with more than 1 service + # Perhaps a list of consumed services for each service definition should be stored + self._store_consumed_service(orchestrated_service, system, method) + + def provided_service( + self, + service_definition: str, + service_uri: str, + interface: str, + method: str, + *func_args, + **func_kwargs, ): + """ + Decorator to add a provided service to the provider. + + Args: + service_definition: Service definition to be stored in the service registry + service_uri: The path to the service + interface: Arrowhead interface string(s) + method: HTTP method required to access the service + """ + provided_service = Service( + service_definition, + service_uri, + interface, + ) + + def wrapped_func(func): + self._provided_services[service_definition] = (provided_service, func) + self.provider.add_provided_service( + service_definition, + service_uri, + method=method, + func=func, + *func_args, + **func_kwargs) + return func + + return wrapped_func + + def run_forever(self) -> None: + """ Start the server, publish all service, and run until interrupted. Then, unregister all services""" + + import warnings + warnings.simplefilter('ignore') + + self._register_all_services() + try: + self._logger.info('Starting server') + print('Started Arrowhead ArrowheadSystem') + self.provider.run_forever() + except KeyboardInterrupt: + self._logger.info('Shutting down server') + print('Shutting down Arrowhead system') + self._unregister_all_services() + finally: + self._logger.info('Server shut down') + + @property + def cert(self) -> Tuple[str, str]: + """ + Tuple of the keyfile and certfile + """ + return self.certfile, self.keyfile + + def _core_system_setup(self) -> None: + """ + Method that sets up the core services. + + Runs when the client is created and should not be run manually. + """ + + self._store_consumed_service( + core_service('register'), + self.config['service_registry'], + 'POST') + self._store_consumed_service( + core_service('unregister'), + self.config['service_registry'], + 'DELETE') + self._store_consumed_service( + core_service('orchestration-service'), + self.config['orchestrator'], + 'POST') + + def _store_consumed_service( + self, + service: Service, + system: ArrowheadSystem, + http_method: str) -> None: + """ + Register consumed services with the consumer + + Args: + service: Service to be stored + system: System containing the service + http_method: HTTP method used to consume the service + """ + + self._consumed_services[service.service_definition] = (service, system, http_method) + + def _register_service(self, service: Service): + """ + Registers the given service with service registry + + Args: + service: Service to register with the Service registry. + """ + + # Decide security level: + if service.interface.secure == 'INSECURE': + secure = 'NOT_SECURE' + elif service.interface.secure == 'SECURE': + secure = 'CERTIFICATE' + else: + secure = 'CERTIFICATE' + # TODO: Add 'TOKEN' security level + + # TODO: Should accept a system and a service + service_registration_form = forms.ServiceRegistrationForm( + provided_service=service, + provider_system=self.system, + # TODO: secure should _NOT_ be hardcoded + secure=secure + ) + + service_registration_response = self.consume_service( + 'register', + json=service_registration_form.dto, + cert=self.cert + ) + + print(service_registration_response.status_code) + print(service_registration_response.text) + # TODO: Error handling + + # TODO: Do logging + + def _register_all_services(self) -> None: + """ + Registers all provided services of the system with the system registry. + """ + for service, _ in self._provided_services.values(): + self._register_service(service) + + def _unregister_service(self, service_definition: str) -> None: + """ + Unregisters the given service with service registry + + Args: + service: Service to unregister with the Service registry. + """ + + if service_definition not in self._provided_services.keys(): + raise ValueError(f'{service_definition} not provided by {self}') + + # TODO: Should be a "form"? + unregistration_payload = { + 'service_definition': service_definition, + 'system_name': self.system.system_name, + 'address': self.system.address, + 'port': self.system.port + } + + service_unregistration_response = self.consume_service( + 'unregister', + params=unregistration_payload, + cert=self.cert + ) + + print(service_unregistration_response.status_code) + + def _unregister_all_services(self) -> None: + """ + Unregisters all provided services of the system with the system registry. + """ + + for service_definition in self._provided_services: + self._unregister_service(service_definition) + + """ + def __enter__(self): + '''Start server and register all services''' + import warnings + warnings.simplefilter('ignore') + + print('Starting server') + self.server.start() + print('Registering services') + self.register_all_services() + + def __exit__(self, exc_type, exc_value, tb): + '''Unregister all services and stop the server''' + if exc_type != KeyboardInterrupt: + print(f'Exception was raised:') + print(exc_value) + + print('\nArrowheadSystem was stopped, unregistering services') + self.unregister_all_services() + print('Stopping server') + self.server.stop() + print('Shutdown completed') + + return True + """ + + +def _service_uri(service: Service, system: ArrowheadSystem) -> str: + service_uri = f'{system.authority}/{service.service_uri}' + + return service_uri diff --git a/arrowhead_client/core_service_forms.py b/arrowhead_client/client/core_service_forms.py similarity index 72% rename from arrowhead_client/core_service_forms.py rename to arrowhead_client/client/core_service_forms.py index c305476..39839a1 100644 --- a/arrowhead_client/core_service_forms.py +++ b/arrowhead_client/client/core_service_forms.py @@ -1,11 +1,12 @@ -#!/usr/bin/env python """ Core Service Forms Module """ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import List, Optional, Dict, Union, Sequence, Mapping +from typing import Optional, Dict, Union, Sequence, Mapping -from . import utils +from arrowhead_client import utils +from arrowhead_client.system import ArrowheadSystem +from arrowhead_client.service import Service class BaseServiceForm(ABC): @@ -49,17 +50,24 @@ def __post_init__(self): @dataclass class ServiceRegistrationForm(CoreSystemServiceForm): """ Service Registration Form """ - service_definition: str - service_uri: str - secure: str - interfaces: Union[Sequence[str], str] - provider_system: BaseServiceForm - metadata: Optional[Mapping[str, str]] = None - end_of_validity: Optional[str] = None - version: Optional[int] = None - def __post_init__(self): - self.interfaces = [self.interfaces] + def __init__( + self, + provided_service: Service, + provider_system: ArrowheadSystem, + secure: str, + metadata: Optional[Mapping[str, str]] = None, + end_of_validity: Optional[str] = None, + version: Optional[int] = None, ): + self.service_definition = provided_service.service_definition + self.service_uri = provided_service.service_uri + self.interfaces = [provided_service.interface.dto] + self.provider_system = provider_system.dto + self.secure = secure + self.metadata = metadata + self.version = version + self.end_of_validity = end_of_validity + class OrchestrationForm(CoreSystemServiceForm): """ Orchestration Form """ @@ -67,16 +75,14 @@ class OrchestrationForm(CoreSystemServiceForm): def __init__(self, requester_system: BaseServiceForm, service_definition_requirement: str, - interface_requirements: Optional[Union[Sequence[str], str]] = None, + interface_requirements: Union[Sequence[str], str] = None, security_requirements: Optional[Union[Sequence[str], str]] = None, metadata_requirements: Optional[Mapping[str, str]] = None, version_requirement: Optional[int] = None, max_version_requirement: Optional[int] = None, min_version_requirement: Optional[int] = None, ping_providers: bool = True, - orchestration_flags: Optional[Mapping[str, bool]] = None, - commands: Optional[Mapping[str, str]] = None, - requester_cloud: Optional[Mapping[str, Union[str, bool, Sequence[int]]]] = None) -> None: + orchestration_flags: Optional[Mapping[str, bool]] = None, ) -> None: self.requester_system = requester_system self.requested_service = ServiceQueryForm( service_definition_requirement, @@ -87,14 +93,9 @@ def __init__(self, max_version_requirement, min_version_requirement, ping_providers).dto - self.commands = commands - self.requester_cloud = requester_cloud - + # TODO: Implement preferred_providers + self.preferred_providers = None if orchestration_flags: self.orchestration_flags = orchestration_flags else: self.orchestration_flags = {'overrideStore': True} - - -if __name__ == "__main__": - pass diff --git a/arrowhead_client/core_service_responses.py b/arrowhead_client/client/core_service_responses.py similarity index 74% rename from arrowhead_client/core_service_responses.py rename to arrowhead_client/client/core_service_responses.py index 2a5f28b..7b6e297 100644 --- a/arrowhead_client/core_service_responses.py +++ b/arrowhead_client/client/core_service_responses.py @@ -1,9 +1,6 @@ from typing import Mapping, Dict, Tuple, List -from collections import namedtuple -from arrowhead_client.system import ArrowheadSystem as System -from arrowhead_client.service import Service as ConsumedHttpService - -ServiceAndSystem = namedtuple('ServiceAndSystem', ['service', 'system']) +from arrowhead_client.system import ArrowheadSystem +from arrowhead_client.service import Service system_keys = ('systemName', 'address', 'port', 'authenticationInfo') service_keys = ('serviceDefinition', 'serviceUri', 'interfaces', 'secure') @@ -27,30 +24,27 @@ def extract_service_data(data: Mapping) -> Dict: return service_data -def handle_service_query_response(service_query_response: Mapping) -> List[ServiceAndSystem]: +def handle_service_query_response(service_query_response: Mapping) -> List[Tuple[Dict, Dict]]: """ Handles service query responses and returns a lists of services and systems """ service_query_data = service_query_response['serviceQueryData'] - services_and_systems = [] - - for data in service_query_data: - service = extract_service_data(data) - system = extract_system_data(data['provider']) - - services_and_systems.append(ServiceAndSystem(service, system)) + service_and_system_list = [ + (extract_service_data(data), extract_system_data(data)) + for data in service_query_data + ] - return services_and_systems + return service_and_system_list -def handle_service_register_response(service_register_response: Mapping) -> NotImplemented: +def handle_service_register_response(service_register_response: Mapping) -> None: """ Handles service register responses """ # TODO: Implement this - return NotImplemented + raise NotImplementedError def handle_orchestration_response(service_orchestration_response: Mapping) \ - -> List[Tuple[ConsumedHttpService, System]]: + -> List[Tuple[Service, ArrowheadSystem]]: """ Turns orchestration response into list of services """ orchestration_response_list = service_orchestration_response['response'] @@ -66,13 +60,13 @@ def handle_orchestration_response(service_orchestration_response: Mapping) \ address = provider_dto['address'] port = provider_dto['port'] - service = ConsumedHttpService( + service = Service( service_definition, service_uri, interface, ) - system = System( + system = ArrowheadSystem( system_name, address, port, diff --git a/arrowhead_client/client/core_services.py b/arrowhead_client/client/core_services.py new file mode 100644 index 0000000..c6cf4c7 --- /dev/null +++ b/arrowhead_client/client/core_services.py @@ -0,0 +1,32 @@ +from typing import Dict +from copy import deepcopy +from arrowhead_client.service import Service + +_http_core_services: Dict[str, Service] = { + 'register': Service( + service_definition='register', + service_uri='serviceregistry/register', + interface='HTTP-SECURE-JSON', ), + 'query': Service( + service_definition='query', + service_uri='serviceregistry/query', + interface='HTTP-SECURE-JSON', ), + 'unregister': Service( + service_definition='unregister', + service_uri='serviceregistry/unregister', + interface='HTTP-SECURE-TEXT', ), + 'orchestration-service': Service( + service_definition='orchestration-service', + service_uri='orchestrator/orchestration', + interface='HTTP-SECURE-JSON', ) +} + + +def core_service(service_defintion: str) -> Service: + core_service_instance = deepcopy(_http_core_services.get(service_defintion, None)) + + if not core_service_instance: + raise ValueError(f'Core service \'{service_defintion}\' not found, ' + f'available core services are {list(_http_core_services.keys())}') + + return core_service_instance diff --git a/arrowhead_client/configuration.py b/arrowhead_client/configuration.py index f010671..afd26c2 100644 --- a/arrowhead_client/configuration.py +++ b/arrowhead_client/configuration.py @@ -1,8 +1,37 @@ +from arrowhead_client.system import ArrowheadSystem +_default_address = '127.0.0.1' + default_config = { - 'service_registry': {'address': '127.0.0.1', 'port': 8443}, - 'orchestrator': {'address': '127.0.0.1', 'port': 8441}, - 'eventhandler': {'address': '127.0.0.1', 'port': 8455}, - 'gatekeeper': {'address': '127.0.0.1', 'port': 8449}, - 'gateway': {'address': '127.0.0.1', 'port': 8453}, } + 'service_registry': + ArrowheadSystem( + 'service_registry', + _default_address, + 8443, + '', ), + 'orchestrator': + ArrowheadSystem( + 'orchestrator', + _default_address, + 8441, + '', ), + 'eventhandler': + ArrowheadSystem( + 'eventhandler', + _default_address, + 8455, + '', ), + 'gatekeeper': + ArrowheadSystem( + 'gatekeeper', + _default_address, + 8449, + '', ), + 'gateway': + ArrowheadSystem( + 'gatekeeper', + _default_address, + 8453, + '', ) +} config = default_config diff --git a/arrowhead_client/consumer.py b/arrowhead_client/consumer.py deleted file mode 100644 index 6a69381..0000000 --- a/arrowhead_client/consumer.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations -from dataclasses import dataclass -from typing import Tuple, Dict, Union - -import requests as backend -from arrowhead_client.service import Service -from arrowhead_client.system import ArrowheadSystem as System - -@dataclass -class Consumer(): - """ Class to create Arrowhead consumer systems """ - - #TODO: Add all arguments instead of using *args - def __init__(self, keyfile, certfile) -> None: - self.keyfile = keyfile - self.certfile = certfile - self._consumed_services: Dict[str, Tuple[Service, System, str]] = {} - - def consume_service(self, service_definition: str, **kwargs) -> backend.Response: - """ Consume registered service """ - # TODO: Add error handling for the case where the service is not - # registered in _consumed_services - - uri, http_method = self._service_uri(service_definition) - - service_response = backend.request(http_method, uri, verify=False, **kwargs) - - return service_response - - #TODO: type ignore above should be removed when mypy issue - # https://github.com/python/mypy/issues/6799 is fixed - - def _service_uri(self, service_definition: str) -> Tuple[str, str]: - service, system, http_method = self._consumed_services[service_definition] - uri = f'https://{system.authority}/{service.service_uri}' - - return uri, http_method - - def _extract_payload(self, service_response, interface) -> Union[Dict, str]: - if interface.payload.upper() == 'JSON': - return service_response.json() - - return service_response.text - diff --git a/arrowhead_client/core_services/__init__.py b/arrowhead_client/core_services/__init__.py deleted file mode 100644 index 6e06ba9..0000000 --- a/arrowhead_client/core_services/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from copy import deepcopy -from .orchestrator import services as orch_services -from .service_registry import services as sr_services -from ..service import Service - -all_core_services = {**sr_services, **orch_services} - - -def core_service(requested_service: str) -> Service: - core_service_instance = deepcopy(all_core_services.get(requested_service, None)) - - if not core_service_instance: - raise ValueError(f'Core service \'{requested_service}\' not found, ' - f'available core services are {list(all_core_services.keys())}') - - return core_service_instance diff --git a/arrowhead_client/core_services/orchestrator.py b/arrowhead_client/core_services/orchestrator.py deleted file mode 100644 index 11f64f7..0000000 --- a/arrowhead_client/core_services/orchestrator.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Dict -from arrowhead_client.service import Service - - -services: Dict[str, Service] = {'orchestration-service': Service( - service_definition='orchestration-service', - service_uri='orchestrator/orchestration', - interface='HTTP-SECURE-JSON',) -} diff --git a/arrowhead_client/core_services/service_registry.py b/arrowhead_client/core_services/service_registry.py deleted file mode 100644 index 12038ae..0000000 --- a/arrowhead_client/core_services/service_registry.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Dict -from arrowhead_client.service import Service - - -#TODO: The service definition is repeated, this dict should be a custom class -# where __getitem__() looks through a list of services. Or not, I'm not sure -services: Dict[str, Service] = { - 'register': Service( - service_definition='register', - service_uri='serviceregistry/register', - interface='HTTP-SECURE-JSON',), - 'query': Service( - service_definition='query', - service_uri='serviceregistry/query', - interface='HTTP-SECURE-JSON',), - 'unregister': Service( - service_definition='unregister', - service_uri='serviceregistry/unregister', - interface='HTTP-SECURE-TEXT',) -} diff --git a/arrowhead_client/httpconsumer.py b/arrowhead_client/httpconsumer.py new file mode 100644 index 0000000..89f3887 --- /dev/null +++ b/arrowhead_client/httpconsumer.py @@ -0,0 +1,32 @@ +from typing import Dict, Union + +import requests as backend +from arrowhead_client.abc import BaseConsumer + + +class HttpConsumer(BaseConsumer): + """ Interface for consumer code """ + + def consume_service(self, service_uri: str, method: str, **kwargs) -> backend.Response: + """ Consume registered service """ + # TODO: Add error handling for the case where the service is not + # registered in _consumed_services + + # Check if cert- and keyfiles are given and use tls if they are. + if kwargs['cert'][0] != '' and kwargs['cert'][1] != '': + service_uri = f'https://{service_uri}' + else: + service_uri = f'http://{service_uri}' + + service_response = backend.request(method, service_uri, verify=False, **kwargs) + + return service_response + + def extract_payload( + self, + service_response: backend.Response, + payload_type: str) -> Union[Dict, str]: + if payload_type.upper() == 'JSON': + return service_response.json() + + return service_response.text diff --git a/arrowhead_client/httpprovider.py b/arrowhead_client/httpprovider.py new file mode 100644 index 0000000..1a57cbf --- /dev/null +++ b/arrowhead_client/httpprovider.py @@ -0,0 +1,36 @@ +from functools import partial +from typing import Callable, Sequence, Any, Mapping +from flask import Flask, request + +from arrowhead_client.abc import BaseProvider + + +class HttpProvider(BaseProvider): + """ Class for service provision """ + + def __init__( + self, + wsgi_server: Any, ) -> None: # type: ignore + self.app = Flask(__name__) + self.wsgi_server = wsgi_server + self.wsgi_server.application = self.app + + def add_provided_service(self, + service_definition: str, + service_uri: str, + method: str, + func: Callable, + *func_args: Sequence[Any], # type: ignore + **func_kwargs: Mapping[Any, Any], # type: ignore + ) -> None: + """ Add service to provider system""" + + # Register service with Flask app + func = partial(func, request, *func_args, **func_kwargs) + self.app.add_url_rule(rule=f'/{service_uri}', + endpoint=service_definition, + methods=[method], + view_func=func) + + def run_forever(self): + self.wsgi_server.serve_forever() diff --git a/arrowhead_client/provider.py b/arrowhead_client/provider.py deleted file mode 100644 index a34f5d0..0000000 --- a/arrowhead_client/provider.py +++ /dev/null @@ -1,60 +0,0 @@ -from functools import partial -from typing import Optional, Callable, Dict, Tuple -from flask import Flask, request -from arrowhead_client.service import Service - - -class Provider(): - """ Class for service provision """ - - def __init__(self) -> None: - self.app = Flask(__name__) # Necessary - self.provided_services: Dict[str, Tuple[Service, Callable]] = {} # Necessary - - def add_provided_service(self, - service_definition: str = '', - service_uri: str = '', - interface: str = '', - provided_service: Service = None, - http_method: str = '', - view_func: Optional[Callable] = None) -> None: - """ Add service to provider system""" - #TODO: This method does two thing at once. It adds a service from parameters, - # and it adds an already created service. These functionalities should be - # separated. Or maybe not? - - if not provided_service: - provided_service = Service( - service_definition, - service_uri, - interface,) - - if provided_service.service_definition not in self.provided_services.keys() and \ - callable(view_func): - # Register service with Flask app - self.provided_services[provided_service.service_definition] = (provided_service, view_func) - view_func = partial(view_func, request) - self.app.add_url_rule(rule=f'/{provided_service.service_uri}', - endpoint=provided_service.service_definition, - methods=[http_method], - view_func=view_func) - else: - # TODO: Add log message when service is trying to be overwritten - pass - - def provided_service(self, - service_definition: str, - service_uri: str, - interface: str, - method: str) -> Callable: - """ Decorator to add provided services """ - def wrapped_func(func): - self.add_provided_service( - service_definition, - service_uri, - interface, - http_method=method, - view_func=func) - return func - return wrapped_func - diff --git a/arrowhead_client/service.py b/arrowhead_client/service.py index 95a9e5e..a6512d9 100644 --- a/arrowhead_client/service.py +++ b/arrowhead_client/service.py @@ -5,6 +5,10 @@ @dataclass() class ServiceInterface: + """ + Service interface triple class + """ + protocol: str secure: str payload: str @@ -34,8 +38,16 @@ def __eq__(self, other: object) -> bool: self.secure == other.secure and \ self.payload == other.payload + class Service(): - """ Base class for services """ + """ + Arrowhead Service class. + + Args: + service_definition: service definition as :code:`str`. + service_uri: service uri location as :code:`str`. + interface: service interface triple, given as :code:`str` (ex. :code:`'HTTP-SECURE-JSON'`) or as :code:`ServiceInterface`. + """ def __init__(self, service_definition: str, @@ -47,9 +59,8 @@ def __init__(self, self.interface = ServiceInterface.from_str(interface) else: self.interface = interface + # TODO: Add security/access policy, metadata, and version fields. def __repr__(self) -> str: variable_string = ', '.join([f'{str(key)}={str(value)}' for key, value in vars(self).items()]) return f'{self.__class__.__name__}({variable_string})' - - diff --git a/arrowhead_client/system.py b/arrowhead_client/system.py index 59b7fd1..b3f79c5 100644 --- a/arrowhead_client/system.py +++ b/arrowhead_client/system.py @@ -1,9 +1,18 @@ +from typing import Dict, Union from dataclasses import dataclass @dataclass class ArrowheadSystem: - """ Basic Arrowhead System class """ + """ + ArrowheadSystem class. + + Args: + system_name: System name as :code:`str`. + address: IP address as :code:`str`. + port: Port as :code:`int`. + authentication_info: Authentication info as :code:`str`. + """ system_name: str address: str @@ -14,12 +23,20 @@ class ArrowheadSystem: def authority(self): return f'{self.address}:{self.port}' - #TODO: from_dto() constructor @property def dto(self): - return_dto = { - 'systemName': self.system_name, - 'address': self.address, - 'port': self.port, - 'authenticationInfo': self.authentication_info} - return return_dto \ No newline at end of file + system_dto = { + 'systemName': self.system_name, + 'address': self.address, + 'port': self.port, + 'authenticationInfo': self.authentication_info} + return system_dto + + @classmethod + def from_dto(cls, system_dto: Dict[str, Union[int, str]]): + return cls( + system_name=str(system_dto['systemName']), + address=str(system_dto['address']), + port=int(system_dto['port']), + authentication_info=str(system_dto['authenticationInfo']) + ) diff --git a/arrowhead_client/utils.py b/arrowhead_client/utils.py index 7f0e348..6cafbed 100644 --- a/arrowhead_client/utils.py +++ b/arrowhead_client/utils.py @@ -11,19 +11,21 @@ def uppercase_strings_in_list(requirement_list: Union[List[str], str]) -> List[s return [requirement_list.upper()] elif isinstance(requirement_list, list): return [requirement.upper() for requirement in requirement_list] + else: + raise TypeError( + "'requirement_list' is type {type(requirement_list)}," + "should be type str or list(str)" + ) def to_camel_case(variable_name: str) -> str: """ Turns snake_case variable name into camelCase """ - split_name = variable_name.split('_') - if split_name[0] == '': - split_name[1] = '_' + split_name[1] - - trailing_underscore = '_' if split_name[-1] == '' else '' + split_name = [split for split in variable_name.split('_') if split != ''] + initial_underscore = '_' if variable_name.startswith('_') else '' + trailing_underscore = '_' if variable_name.endswith('_') else '' - return split_name[0] + \ - ''.join([split.capitalize() for split in split_name[1:]]) + \ - trailing_underscore + return initial_underscore + split_name[0] + \ + ''.join([split.capitalize() for split in split_name[1:]]) + trailing_underscore def to_snake_case(variable_name: str) -> str: @@ -33,5 +35,4 @@ def to_snake_case(variable_name: str) -> str: trailing_underscore = '_' if variable_name.endswith('_') else '' return initial_underscore + \ - '_'.join([camel.lower() for camel in split_camel]) + \ - trailing_underscore + '_'.join([camel.lower() for camel in split_camel]) + trailing_underscore diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..6247f7e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 0000000..069405d --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,35 @@ +************* +API Reference +************* + +.. automodule:: arrowhead_client.api + +ArrowheadHttpClient +^^^^^^^^^^^^^^^^^^^ +.. autoclass:: arrowhead_client.api.ArrowheadHttpClient + :members: + +ArrowheadClient +^^^^^^^^^^^^^^^ +.. autoclass:: arrowhead_client.api.ArrowheadClient + :members: + +ArrowheadSystem +^^^^^^^^^^^^^^^ +.. autoclass:: arrowhead_client.api.ArrowheadSystem + :members: + +Service +^^^^^^^ +.. autoclass:: arrowhead_client.api.Service + :members: + +HttpConsumer +^^^^^^^^^^^^ +.. autoclass:: arrowhead_client.api.HttpConsumer + :members: + +HttpProvider +^^^^^^^^^^^^ +.. autoclass:: arrowhead_client.api.HttpProvider + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..b03fb25 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,61 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# 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 +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../../')) +import _version + + +# -- Project information ----------------------------------------------------- + +project = _version.__lib_name__ +copyright = f'2020, {_version.__author__}' +author = _version.__author__ + +# The full version, including alpha/beta/rc tags +release = _version.__version__ + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.napoleon', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +import sphinx_theme +html_theme = 'stanford_theme' +html_theme_path = [sphinx_theme.get_html_theme_path('stanford-theme')] + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..1d679ef --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,23 @@ +############################################ +Welcome to arrowhead-client's documentation! +############################################ + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + + quickstart + tut-trouble + security + todo + api + +****************** +Indices and tables +****************** + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 0000000..6f6b49c --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,94 @@ +Arrowhead Client library quickstart +=================================== +.. warning:: + + This document is a work in progress. + +Setting up a local cloud +------------------------ + +You need to have an Arrowhead local cloud running and set up properly before running attempting this quickstart. + +Getting a provider running +-------------------------- + +The provider provides services within a local cloud. +To get a provider running start by importing the client library. + +.. literalinclude:: ../../examples/provider_app.py + :language: python + :lines: 4 + +Then we can create an application containing a system and provider. + +.. literalinclude:: ../../examples/provider_app.py + :language: python + :lines: 6-12 + +To add a service, we decorate a function with the `provided_service` decorator, and the service information. + +.. literalinclude:: ../../examples/provider_app.py + :language: python + :lines: 15-21 + +Everything is now inplace, we just need to start the application with the `run_forever` method. + +.. literalinclude:: ../../examples/provider_app.py + :language: python + :lines: 23-24 + +This method will start the application, register the services with service registry and simply serve until we terminate +the app with ctrl+c. + +And that's it! In 15 lines of code (6 if you make the code hard to read) +we have set up a provider and are currently running it! +Here is the full code listing for this example. + +examples/provider_app.py +^^^^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/provider_app.py + :language: python + + +Getting a consumer running +-------------------------- + +It's just as easy to get a consumer going, we start in a similar way. + +.. literalinclude:: ../../examples/consumer_app.py + :language: python + :lines: 4-12 + +Now, instead of adding a provided service, we tell it what service to consume with the `add_consumed_service` method. + +.. literalinclude:: ../../examples/consumer_app.py + :language: python + :lines: 14 + +To consume the service, we only need to call the `consume_service` method with the name of the service we want to consume. + +.. literalinclude:: ../../examples/consumer_app.py + :language: python + :lines: 16-18 + +And the full listing: + +examples/consumer_app.py +^^^^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/consumer_app.py + :language: python + +If you first start the provider application and then run the consumer application, the your terminal should print + +.. code:: shell + + Hello, Arrowhead! + +Beyond the tutorial +------------------- + +Congratulations, you have created your first Arrowhead services and systems in using the arrowhead-client library! +This tutorial is meant to show you the basics of using the library without any bells and whistles, just a simple pair +of providers and consumers. + +If you had trouble getting these examples up and running, please look at :ref:`tutorial_trouble`. diff --git a/docs/source/security.rst b/docs/source/security.rst new file mode 100644 index 0000000..1f1139b --- /dev/null +++ b/docs/source/security.rst @@ -0,0 +1,39 @@ +Security settings +================= + +The :code:`ArrowheadHttpClient` supports the CERTIFICATE and NOT_SECURE security modes. + +CERTIFICATE mode +^^^^^^^^^^^^^^^^ +To use the CERTIFICATE mode, provide a certfile and keyfile to the constructor. +If you're using a pkcs12 keystore, it needs to be converted to a certfile and keyfile first. + +Example: + +.. code-block:: python + + ArrowheadHttpClient( + system_name='Secure', + address='127.0.0.1', + port=1337, + keyfile='path/to/secure.key', + certfile='path/to/secure.crt', + ) + +NOT_SECURE mode +^^^^^^^^^^^^^^^ +To use the NOT_SECURE mode, don't provide any certfile or keyfile. + +Example: + +.. code-block:: python + + ArrowheadHttpClient( + system_name='Insecure', + address='127.0.0.1', + port=1338 + ) + +TOKEN mode +^^^^^^^^^^ +The python client does not yet support the TOKEN security mode. \ No newline at end of file diff --git a/docs/source/todo.rst b/docs/source/todo.rst new file mode 100644 index 0000000..24bf399 --- /dev/null +++ b/docs/source/todo.rst @@ -0,0 +1,37 @@ +TODO-list +========= + +Quality of Life +^^^^^^^^^^^^^^^ + +- Make it easy to provide and consume services simultaneously. + +Service consumation +^^^^^^^^^^^^^^^^^^^ + +- Error handling when service could not be consumed. + +Service Registry +^^^^^^^^^^^^^^^^ + +- Error handling when registrating and unregistrating a service. + +Orchestration +^^^^^^^^^^^^^ + +- Error handling when no services are returned from orchestration request. +- Support for multiple returned services from orchestration request. +- Give proper support for orchestration flags, current praxis is to leave that field empty which defaults to using the :code:`overrideStore` flag. + +Security +^^^^^^^^ + +- Implement the TOKEN security mode. + +Logging +^^^^^^^ + +- Implement logging for: + - Service registration errors. + - Orchestration error. + - Service consumation errors. \ No newline at end of file diff --git a/docs/source/tut-trouble.rst b/docs/source/tut-trouble.rst new file mode 100644 index 0000000..dc569d8 --- /dev/null +++ b/docs/source/tut-trouble.rst @@ -0,0 +1,6 @@ +.. _tutorial_trouble: +Tutorial Troubleshooting and FAQ +================================ + +This page is currently empty due to lack of questions and troubles. +We expect this page to be filled with content when questions start rolling in. \ No newline at end of file diff --git a/examples/consumer_app.py b/examples/consumer_app.py new file mode 100644 index 0000000..8986783 --- /dev/null +++ b/examples/consumer_app.py @@ -0,0 +1,21 @@ +""" +HttpConsumer example app +""" +import arrowhead_client.api as ar + +consumer_app = ar.ArrowheadHttpClient( + system_name='example-consumer', + address='127.0.0.1', + port=7656, + keyfile='certificates/example-consumer.key', + certfile='certificates/example-consumer.crt', +) + +consumer_app.add_consumed_service('hello-arrowhead', 'GET') + +if __name__ == '__main__': + response = consumer_app.consume_service('hello-arrowhead') + message = consumer_app.consumer.extract_payload(response, 'json') + message_2 = consumer_app.extract_payload(response, 'json') # TODO: remove the first message extraction + + print(message['msg']) diff --git a/examples/provider_app.py b/examples/provider_app.py new file mode 100644 index 0000000..684825e --- /dev/null +++ b/examples/provider_app.py @@ -0,0 +1,24 @@ +""" +HttpProvider example app +""" +import arrowhead_client.api as ar + +provider_app = ar.ArrowheadHttpClient( + system_name='example-provider', + address='127.0.0.1', + port=7655, + keyfile='certificates/example-provider.key', + certfile='certificates/example-provider.crt', +) + + +@provider_app.provided_service( + 'hello-arrowhead', + 'hello', + 'HTTP-INSECURE-JSON', + 'GET', ) +def hello_arrowhead(request): + return {"msg": "Hello, Arrowhead!"} + +if __name__ == '__main__': + provider_app.run_forever() diff --git a/examples/time_consumer.py b/examples/time_consumer.py index 7eeb075..95b4d12 100644 --- a/examples/time_consumer.py +++ b/examples/time_consumer.py @@ -1,6 +1,6 @@ -from arrowhead_client.api import ArrowheadHttpApplication +from arrowhead_client.api import ArrowheadHttpClient -time_consumer = ArrowheadHttpApplication( +time_consumer = ArrowheadHttpClient( system_name='consumer_test', address='localhost', port=1338, @@ -8,8 +8,8 @@ keyfile='certificates/consumer_test.key', certfile='certificates/consumer_test.crt') -time_consumer.add_consumed_service('echo', http_method='GET') -time_consumer.add_consumed_service('hej', http_method='POST') +time_consumer.add_consumed_service('echo', method='GET') +time_consumer.add_consumed_service('hej', method='POST') if __name__ == '__main__': echo_response = time_consumer.consume_service('echo') diff --git a/examples/time_provider.py b/examples/time_provider.py index e2c7d23..1c4ed4a 100644 --- a/examples/time_provider.py +++ b/examples/time_provider.py @@ -1,20 +1,24 @@ from typing import Dict import arrowhead_client.api as ar -time_provider = ar.ArrowheadSystem('time_provider', +time_provider = ar.ArrowheadHttpClient('time_provider', 'localhost', 1337, '', keyfile='certificates/time_provider.key', certfile='certificates/time_provider.crt') +what_to_hello = 'Arrowhead' -def echo(request) -> Dict[str, str]: - return {'msg': 'Hello Arrowhead'} +@time_provider.provided_service('echo', 'echo', 'HTTP-SECURE-JSON', 'GET', hello_what=what_to_hello) +def echo(request, hello_what) -> Dict[str, str]: + return {'msg': f'Hello {hello_what}'} +@time_provider.provided_service('hej', 'hej', 'HTTP-SECURE-JSON', 'POST') def post(request) -> Dict[str, str]: print(request.json) + what_to_hello = request.json['what_to_hello'] return {'response': 'OK'} @@ -24,29 +28,4 @@ def decorator(request) -> Dict[str, str]: if __name__ == '__main__': - time_provider.add_provided_service( - service_definition='echo', - service_uri='echo', - interface='HTTP-SECURE-JSON', - http_method='GET', - view_func=echo - ) - - time_provider.add_provided_service( - service_definition='hej', - service_uri='hej', - interface='HTTP-SECURE-JSON', - http_method='POST', - view_func=post - ) - - time_provider.add_provided_service( - 'lambda', - 'lambda', - 'HTTP-SECURE-JSON', - http_method='GET', - view_func=lambda: {'lambda': True} - ) - - print(time_provider.certfile) time_provider.run_forever() diff --git a/examples/time_provider_advanced.py b/examples/time_provider_advanced.py index d54c749..ac5caca 100644 --- a/examples/time_provider_advanced.py +++ b/examples/time_provider_advanced.py @@ -1,5 +1,5 @@ import datetime -from arrowhead_client.application import ProviderSystem +from arrowhead_client.client import ProviderSystem from flask import request #from source.service_provider import ServiceProvider diff --git a/setup.py b/setup.py index 9d2f0d6..366cbd2 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,25 @@ import setuptools +import _version with open('README.md', 'r') as fh: long_description = fh.read() setuptools.setup( - name='arrowhead-client', - version='0.1.1a1', - author='Jacob Nilsson', - author_email='jacob.nilsson@ltu.se', - description='Arrowhead system and service client library', + name=_version.__lib_name__, + version=_version.__version__, + author=_version.__author__, + author_email=_version.__email__, + description='Arrowhead client library', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/arrowhead-f/client-library-python', packages=setuptools.find_packages(exclude=['tests', 'examples']), - licence='EPL-2.0', + license='EPL-2.0', install_requires=[ 'Flask>=1.0.2', 'requests>=2.21', - 'gevent>=20.5.0' + 'gevent>=20.5.0', + 'typing-extensions>=3.7' ], classifiers=[ 'Development Status :: 3 - Alpha', @@ -25,6 +27,7 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Operating System :: POSIX :: Linux', 'License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)', ], diff --git a/tests/test_forms.py b/tests/test_forms.py index 4224a43..03697e6 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,21 +1,24 @@ -import unittest from arrowhead_client.system import ArrowheadSystem -import arrowhead_client.core_service_forms as forms +from arrowhead_client.service import Service +import arrowhead_client.client.core_service_forms as forms requester_system = ArrowheadSystem('test_system', 'localhost', 0, '') provider_system = ArrowheadSystem('test_system', 'localhost', 0, '') def test_registration_form(): - registration_form = forms.ServiceRegistrationForm( + service = Service( 'test_service', - '/test', - 'CERTIFICATE', - ['HTTP-SECURE-JSON'], - provider_system.dto, - {'dummy': 'data'}, - 'dummy-date', - 0, + '/test/test/test', + 'HTTP-SECURE-JSON', + ) + registration_form = forms.ServiceRegistrationForm( + provided_service=service, + provider_system=provider_system, + secure='CERTIFICATE', + metadata={'dummy': 'data'}, + end_of_validity='dummy-date', + version=0, ) valid_keys = { @@ -44,7 +47,6 @@ def test_orchestration_form(): 0, True, {'dummy': True}, - {'dummy': 'data'}, ) valid_keys = { diff --git a/tests/test_response_handling.py b/tests/test_response_handling.py index 4dd4b95..e605de0 100644 --- a/tests/test_response_handling.py +++ b/tests/test_response_handling.py @@ -1,8 +1,10 @@ -from arrowhead_client import core_service_responses as csr +import pytest +from arrowhead_client.client import core_service_responses as csr def test_registration_response(): - csr.handle_service_register_response({'dummy': 'data'}) + with pytest.raises(Exception) as e: + csr.handle_service_register_response({'dummy': 'data'}) diff --git a/tests/test_system.py b/tests/test_system.py index 3f8f597..da0759f 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -1,20 +1,36 @@ from arrowhead_client.system import ArrowheadSystem +valid_keys = { + 'systemName', + 'address', + 'port', + 'authenticationInfo' +} + def test_arrowhead_system(): - test_consumer = ArrowheadSystem( - 'test_consumer', + test_system = ArrowheadSystem( + 'test_system', '127.0.0.1', 0, '', ) - valid_keys = { - 'systemName', - 'address', - 'port', - 'authenticationInfo' + + assert test_system.dto.keys() == valid_keys + assert test_system.authority == '127.0.0.1:0' + +def test_from_dto(): + dto = { + "systemName": "test_system", + "address": "127.0.0.1", + "port": 0, + "authenticationInfo": "look away" } - assert test_consumer.dto.keys() == valid_keys - assert test_consumer.authority == '127.0.0.1:0' + test_system = ArrowheadSystem.from_dto(dto) + + assert test_system.dto.keys() == valid_keys + assert test_system.authority == '127.0.0.1:0' + assert test_system.system_name == 'test_system' + assert test_system.authentication_info == 'look away' diff --git a/tox.ini b/tox.ini index b04e6b0..893d5e1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,9 @@ [tox] -envlist = py37, py38 +envlist = py37, py38, py39, docs +isolated_build = True + +[flake8] +ignore = E501, E126, E127 [testenv] changedir=tests @@ -7,7 +11,7 @@ passenv=* deps= pytest mypy - pyflakes + flake8 httmock Flask>=1.0.2 requests>=2.21 @@ -15,5 +19,20 @@ deps= usedevelop=True commands= pytest - mypy ../arrowhead_client/__init__.py - pyflakes ../arrowhead_client + mypy ../arrowhead_client/ + flake8 ../arrowhead_client/ + +[testenv:py37] +typing-extensions>=3.7 + +[testenv:docs] +description = invoke sphinx-build to build the docs +basepython = python3.8 +changedir = docs +deps = + sphinx >= 3.0 + sphinx_theme +commands = + sphinx-build ./source ./build/html + sphinx-build ./source ./build/latex/ -b latex + make latexpdf \ No newline at end of file