diff --git a/connect/config.py b/connect/config.py index d6b52cd..e37dd0b 100644 --- a/connect/config.py +++ b/connect/config.py @@ -8,25 +8,29 @@ import json import os +from typing import List, Union + class Config(object): - _instance = None # Global instance + # Global instance + _instance = None # type: Config # noinspection PyShadowingBuiltins def __init__( self, - api_url=None, - api_key=None, - products=None, - file=None + api_url=None, # type: str + api_key=None, # type: str + products=None, # type: Union[str, List[str]] + file=None # type: str ): """ - initialization config for public api + Initialization config for public api :param api_url: Public api url :param api_key: Service user ApiKey :param products (optional): Id products - :param file: Config file path + :param file: Config file name """ + # Check arguments if not file and not any([api_key, api_url]): raise ValueError('Filename or api_key and api_url are expected' @@ -71,18 +75,22 @@ def __init__( @classmethod def get_instance(cls): + # type: () -> Config if not cls._instance: cls._instance = Config(file='config.json') return cls._instance @property def api_url(self): + # type: () -> str return self._api_url @property def api_key(self): + # type: () -> str return self._api_key @property def products(self): + # type: () -> List[str] return self._products diff --git a/connect/models/__init__.py b/connect/models/__init__.py index f97ff6e..62fc8c7 100644 --- a/connect/models/__init__.py +++ b/connect/models/__init__.py @@ -9,6 +9,7 @@ from .base import BaseSchema from .fulfillment import FulfillmentSchema from .parameters import Param, ParamSchema +from .product import Item, ItemSchema from .server_error import ServerErrorSchema __all__ = [ @@ -16,7 +17,9 @@ 'ActivationTileResponse', 'BaseSchema', 'FulfillmentSchema', - 'ServerErrorSchema', + 'Item', + 'ItemSchema', 'Param', 'ParamSchema', + 'ServerErrorSchema', ] diff --git a/connect/models/activation_response.py b/connect/models/activation_response.py index ef91f6d..bfbca72 100644 --- a/connect/models/activation_response.py +++ b/connect/models/activation_response.py @@ -9,16 +9,19 @@ class ActivationTileResponse(object): - tile = 'Activation succeeded' + tile = 'Activation succeeded' # type: str def __init__(self, markdown=None): - if markdown: - try: - self.tile = json.loads(markdown) - except ValueError: - self.tile = markdown + # type: (str) -> None + try: + self.tile = json.loads(markdown) + except ValueError: + self.tile = markdown or self.__class__.tile class ActivationTemplateResponse(object): + template_id = None # type: str + def __init__(self, template_id): + # type: (str) -> None self.template_id = template_id diff --git a/connect/models/asset.py b/connect/models/asset.py index 7e707eb..b31e450 100644 --- a/connect/models/asset.py +++ b/connect/models/asset.py @@ -6,25 +6,33 @@ """ from marshmallow import fields, post_load +from typing import List from .base import BaseModel, BaseSchema -from .connection import ConnectionSchema -from .parameters import ParamSchema -from .product import ItemSchema, ProductSchema -from .tiers import TiersSchemaMixin +from .connection import Connection, ConnectionSchema +from .parameters import Param, ParamSchema +from .product import Item, ItemSchema, Product, ProductSchema +from .tiers import Tiers, TiersSchema class Asset(BaseModel): + status = None # type: str + external_id = None # type: str + external_uid = None # type: str + product = None # type: Product + connection = None # type: Connection + items = None # type: List[Item] + params = None # type: List[Param] + tiers = None # type: Tiers + def get_param_by_id(self, id_): try: - # noinspection PyUnresolvedReferences return list(filter(lambda param: param.id == id_, self.params))[0] except IndexError: return None def get_item_by_mpn(self, mpn): try: - # noinspection PyUnresolvedReferences return list(filter(lambda item: item.mpn == mpn, self.items))[0] except IndexError: return None @@ -40,7 +48,7 @@ class AssetSchema(BaseSchema): ) items = fields.List(fields.Nested(ItemSchema)) params = fields.List(fields.Nested(ParamSchema)) - tiers = fields.Nested(TiersSchemaMixin) + tiers = fields.Nested(TiersSchema) @post_load def make_object(self, data): diff --git a/connect/models/base.py b/connect/models/base.py index b1fda32..0a191a3 100644 --- a/connect/models/base.py +++ b/connect/models/base.py @@ -9,11 +9,12 @@ class BaseModel: + id = None # type: str + def __init__(self, **kwargs): - self.id = kwargs.get('id') - if kwargs: - for attr, val in kwargs.items(): - setattr(self, attr, val) + # Inject parsed properties in the model + for attr, val in kwargs.items(): + setattr(self, attr, val) class BaseSchema(Schema): diff --git a/connect/models/company.py b/connect/models/company.py index 8f3d84a..25ac4cd 100644 --- a/connect/models/company.py +++ b/connect/models/company.py @@ -11,7 +11,7 @@ class Company(BaseModel): - pass + name = None # type: str class CompanySchema(BaseSchema): diff --git a/connect/models/connection.py b/connect/models/connection.py index 5f63a6b..2721b1e 100644 --- a/connect/models/connection.py +++ b/connect/models/connection.py @@ -8,13 +8,17 @@ from marshmallow import fields, post_load from .base import BaseModel, BaseSchema -from .company import CompanySchema -from .hub import HubSchema -from .product import ProductSchema +from .company import Company, CompanySchema +from .hub import Hub, HubSchema +from .product import Product, ProductSchema class Connection(BaseModel): - pass + type = None # type: str + provider = None # type: Company + vendor = None # type: Company + product = None # type: Product + hub = None # type: Hub class ConnectionSchema(BaseSchema): diff --git a/connect/models/contact.py b/connect/models/contact.py index 92697ff..50cf3a7 100644 --- a/connect/models/contact.py +++ b/connect/models/contact.py @@ -11,7 +11,10 @@ class PhoneNumber(BaseModel): - pass + country_code = None # type: str + area_code = None # type: str + phone_number = None # type: str + extension = None # type: str class PhoneNumberSchema(BaseSchema): @@ -26,7 +29,10 @@ def make_object(self, data): class Contact(BaseModel): - pass + email = None # type: str + first_name = None # type: str + last_name = None # type: str + phone_number = None # type: PhoneNumber class ContactSchema(BaseSchema): @@ -41,7 +47,13 @@ def make_object(self, data): class ContactInfo(BaseModel): - pass + address_line1 = None # type: str + address_line2 = None # type: str + city = None # type: str + contact = None # type: Contact + country = None # type: str + postal_code = None # type: str + state = None # type: str class ContactInfoSchema(BaseSchema): diff --git a/connect/models/exception.py b/connect/models/exception.py index e2ebc0a..ffcba81 100644 --- a/connect/models/exception.py +++ b/connect/models/exception.py @@ -4,12 +4,18 @@ This file is part of the Ingram Micro Cloud Blue Connect SDK. Copyright (c) 2019 Ingram Micro. All Rights Reserved. """ +from typing import List +from connect.models import Param from .server_error import ServerError class Message(Exception): + code = None # type: str + obj = None # type: object + def __init__(self, message='', code='', obj=None): + # type: (str, str, object) -> None self.message = message self.code = code self.obj = obj @@ -17,13 +23,17 @@ def __init__(self, message='', code='', obj=None): class FulfillmentFail(Message): def __init__(self, *args, **kwargs): + # type: (*any, **any) -> None super(FulfillmentFail, self).__init__(*args, **kwargs) self.message = self.message or 'Request failed' self.code = 'fail' class FulfillmentInquire(Message): + params = None # type: List[Param] + def __init__(self, *args, **kwargs): + # type: (*any, **any) -> None super(FulfillmentInquire, self).__init__(*args, **kwargs) self.message = self.message or 'Correct user input required' self.code = 'inquire' @@ -32,21 +42,25 @@ def __init__(self, *args, **kwargs): class Skip(Message): def __init__(self, *args, **kwargs): + # type: (*any, **any) -> None super(Skip, self).__init__(*args, **kwargs) self.message = self.message or 'Request skipped' self.code = 'skip' class ServerErrorException(Exception): - message = 'Server error' + message = 'Server error' # type: str def __init__(self, error=None, *args, **kwargs): + # type: (ServerError, *any, **any) -> None + if error and isinstance(error, ServerError): - # noinspection PyUnresolvedReferences self.message = str({ "error_code": error.error_code, "params": kwargs.get('params', []), "errors": error.errors, }) + else: + self.message = self.__class__.message super(ServerErrorException, self).__init__(self.message, *args) diff --git a/connect/models/fulfillment.py b/connect/models/fulfillment.py index 6c5dd17..6f26a45 100644 --- a/connect/models/fulfillment.py +++ b/connect/models/fulfillment.py @@ -7,12 +7,23 @@ from marshmallow import fields, post_load -from .asset import AssetSchema +from .asset import Asset, AssetSchema from .base import BaseModel, BaseSchema -from .marketplace import ContractSchema, MarketplaceSchema +from .marketplace import Contract, ContractSchema, Marketplace, MarketplaceSchema class Fulfillment(BaseModel): + activation_key = None # type: str + asset = None # type: Asset + status = None # type: str + type = None # type: str + updated = None # type: str + created = None # type: str + reason = None # type: str + params_from_url = None # type: str + contract = None # type: Contract + marketplace = None # type: Marketplace + @property def new_items(self): # noinspection PyUnresolvedReferences diff --git a/connect/models/hub.py b/connect/models/hub.py index cfec109..485952f 100644 --- a/connect/models/hub.py +++ b/connect/models/hub.py @@ -11,7 +11,7 @@ class Hub(BaseModel): - pass + name = None # type: str class HubSchema(BaseSchema): @@ -22,6 +22,15 @@ def make_object(self, data): return Hub(**data) -class HubsSchemaMixin(Schema): +class Hubs(BaseModel): + hub = None # type: Hub + external_id = None # type: str + + +class HubsSchema(Schema): hub = fields.Nested(HubSchema, only=('id', 'name')) external_id = fields.Str() + + @post_load + def make_object(self, data): + return Hubs(**data) diff --git a/connect/models/marketplace.py b/connect/models/marketplace.py index 422bf9f..ed462ea 100644 --- a/connect/models/marketplace.py +++ b/connect/models/marketplace.py @@ -8,12 +8,18 @@ from marshmallow import fields, post_load from .base import BaseModel, BaseSchema -from .company import CompanySchema -from .hub import HubsSchemaMixin +from .company import Company, CompanySchema +from .hub import Hubs, HubsSchema class Marketplace(BaseModel): - pass + name = None # type: str + zone = None # type: str + description = None # type: str + active_contract = None # type: int + icon = None # type: str + owner = None # type: Company + hubs = None # type: Hubs class MarketplaceSchema(BaseSchema): @@ -23,7 +29,7 @@ class MarketplaceSchema(BaseSchema): active_contract = fields.Int() icon = fields.Str() owner = fields.Nested(CompanySchema, only=('id', 'name')) - hubs = fields.List(fields.Nested(HubsSchemaMixin, only=('id', 'name'))) + hubs = fields.List(fields.Nested(HubsSchema, only=('id', 'name'))) @post_load def make_object(self, data): @@ -31,7 +37,18 @@ def make_object(self, data): class Agreement(BaseModel): - pass + type = None # type: str + title = None # type: str + description = None # type: str + created = None # type: str + updated = None # type: str + owner = None # type: Company + stats = None # type: dict + active = None # type: bool + version = None # type: int + link = None # type: str + version_created = None # type: str + version_contracts = None # type: int class AgreementSchema(BaseSchema): @@ -54,7 +71,20 @@ def make_object(self, data): class Contract(BaseModel): - pass + name = None # type: str + status = None # type: str + version = None # type: int + type = None # type: str + agreement = None # type: Agreement + marketplace = None # type: Marketplace + owner = None # type: Company + creater = None # type: Company + created = None # type: str + updated = None # type: str + enrolled = None # type: str + version_created = None # type: str + activation = None # type: dict + signee = None # type: Company class ContractSchema(BaseSchema): diff --git a/connect/models/parameters.py b/connect/models/parameters.py index 0633680..3a990f5 100644 --- a/connect/models/parameters.py +++ b/connect/models/parameters.py @@ -6,12 +6,14 @@ """ from marshmallow import Schema, fields, post_load +from typing import List from .base import BaseModel, BaseSchema class ValueChoice(BaseModel): - pass + value = None # type: str + label = None # type: str class ValueChoiceSchema(Schema): @@ -24,7 +26,11 @@ def make_object(self, data): class Param(BaseModel): - pass + name = None # type: str + type = None # type: str + value = None # type: str + value_choices = None # type: List[ValueChoice] + value_error = None # type: str class ParamSchema(BaseSchema): diff --git a/connect/models/product.py b/connect/models/product.py index 33cdebc..12169ed 100644 --- a/connect/models/product.py +++ b/connect/models/product.py @@ -11,7 +11,7 @@ class Product(BaseModel): - pass + name = None # type: str class ProductSchema(BaseSchema): @@ -23,7 +23,10 @@ def make_object(self, data): class Item(BaseModel): - pass + global_id = None # type: str + mpn = None # type: str + old_quantity = None # type: int + quantity = None # type: int class ItemSchema(BaseSchema): diff --git a/connect/models/server_error.py b/connect/models/server_error.py index 5795c5d..c29bfb9 100644 --- a/connect/models/server_error.py +++ b/connect/models/server_error.py @@ -6,12 +6,15 @@ """ from marshmallow import Schema, fields, post_load +from typing import List from .base import BaseModel class ServerError(BaseModel): - pass + error_code = None # type: str + params = None # type: dict + errors = None # type: List[str] class ServerErrorSchema(Schema): diff --git a/connect/models/tiers.py b/connect/models/tiers.py index 554051b..d9c5ed5 100644 --- a/connect/models/tiers.py +++ b/connect/models/tiers.py @@ -8,11 +8,14 @@ from marshmallow import Schema, fields, post_load from .base import BaseModel, BaseSchema -from .contact import ContactInfoSchema +from .contact import ContactInfo, ContactInfoSchema class Tier(BaseModel): - pass + name = None # type: str + contact_info = None # type: ContactInfo + external_id = None # type: str + external_uid = None # type: str class TierSchema(BaseSchema): @@ -26,7 +29,17 @@ def make_object(self, data): return Tier(**data) -class TiersSchemaMixin(Schema): +class Tiers(BaseModel): + customer = None # type: Tier + tier1 = None # type: Tier + tier2 = None # type: Tier + + +class TiersSchema(Schema): customer = fields.Nested(TierSchema) tier1 = fields.Nested(TierSchema) tier2 = fields.Nested(TierSchema) + + @post_load + def make_object(self, data): + return Tiers(**data) diff --git a/connect/resource/base.py b/connect/resource/base.py index 35316d2..ee75f88 100644 --- a/connect/resource/base.py +++ b/connect/resource/base.py @@ -6,26 +6,35 @@ """ import requests +from typing import Any, List, Dict from connect.config import Config from connect.logger import function_log, logger from connect.models import BaseSchema, ServerErrorSchema +from connect.models.base import BaseModel from connect.models.exception import ServerErrorException from .utils import join_url class ApiClient(object): - def __init__(self, config=None): + # type: (Config) -> None + # Assign passed config or globally configured instance - self.config = config or Config.get_instance() + self._config = config or Config.get_instance() # Assert data if not isinstance(self.config, Config): raise ValueError('A valid Config object is required to create an ApiClient') + @property + def config(self): + # type: () -> Config + return self._config + @property def headers(self): + # type: () -> Dict[str, str] return { "Authorization": self.config.api_key, "Content-Type": "application/json", @@ -33,6 +42,7 @@ def headers(self): @staticmethod def check_response(response): + # type: (requests.Response) -> str if not hasattr(response, 'content'): raise AttributeError( 'Response not attribute content. Check your request params' @@ -73,7 +83,7 @@ class BaseResource(object): def __init__(self, config=None): # Assign passed config or globally configured instance - self.config = config or Config.get_instance() + self._config = config or Config.get_instance() # Assert data if not self.__class__.resource: @@ -85,7 +95,28 @@ def __init__(self, config=None): if not BaseResource.api: BaseResource.api = ApiClient(config) + @property + def config(self): + # type: () -> Config + return self._config + + @property + def list(self): + # type: () -> List[Any] + filters = self.build_filter() + logger.info('Get list request by filter - {}'.format(filters)) + response = self.api.get(url=self._list_url, params=filters) + return self.__loads_schema(response) + + def get(self, pk): + # type: (str) -> Any + response = self.api.get(url=self._obj_url(pk)) + objects = self.__loads_schema(response) + if isinstance(objects, list) and len(objects) > 0: + return objects[0] + def build_filter(self): + # type: () -> Dict[str, Any] res_filter = {} if self.limit: res_filter['limit'] = self.limit @@ -94,12 +125,15 @@ def build_filter(self): @property def _list_url(self): + # type: () -> str return join_url(self.config.api_url, self.__class__.resource) def _obj_url(self, pk): + # type: (str) -> str return join_url(self._list_url, pk) def __loads_schema(self, response): + # type: (str) -> List[BaseModel] objects, error = self.schema.loads(response, many=True) if error: raise TypeError( @@ -108,15 +142,3 @@ def __loads_schema(self, response): ) return objects - - def get(self, pk): - response = self.api.get(url=self._obj_url(pk)) - objects = self.__loads_schema(response) - if isinstance(objects, list) and len(objects) > 0: - return objects[0] - - def list(self): - filters = self.build_filter() - logger.info('Get list request by filter - {}'.format(filters)) - response = self.api.get(url=self._list_url, params=filters) - return self.__loads_schema(response) diff --git a/connect/resource/fulfillment.py b/connect/resource/fulfillment.py index fbbfac0..2c12344 100644 --- a/connect/resource/fulfillment.py +++ b/connect/resource/fulfillment.py @@ -7,8 +7,10 @@ import json +from typing import List + from connect.logger import function_log -from connect.models import FulfillmentSchema, Param +from connect.models import FulfillmentSchema, Param, ActivationTileResponse from .base import BaseResource from .template import TemplateResource from .utils import join_url @@ -20,6 +22,7 @@ class FulfillmentResource(BaseResource): schema = FulfillmentSchema() def build_filter(self): + # type: () -> dict filters = super(FulfillmentResource, self).build_filter() if self.config.products: filters['asset.product.id__in'] = ','.join(self.config.products) @@ -29,24 +32,29 @@ def build_filter(self): @function_log def approve(self, pk, data): + # type: (str, dict) -> str url = join_url(self._obj_url(pk), 'approve/') return self.api.post(url=url, data=json.dumps(data if data else {})) @function_log def inquire(self, pk): + # type: (str) -> str return self.api.post(url=join_url(self._obj_url(pk), 'inquire/'), data=json.dumps({})) @function_log def fail(self, pk, reason): + # type: (str, str) -> str url = join_url(self._obj_url(pk), 'fail/') return self.api.post(url=url, data=json.dumps({'reason': reason})) @function_log def render_template(self, pk, template_id): + # type: (str, str) -> ActivationTileResponse return TemplateResource(self.config).render(template_id, pk) @function_log def update_parameters(self, pk, params): + # type: (str, List[Param]) -> str list_dict = [] for _ in params: list_dict.append(_.__dict__ if isinstance(_, Param) else _) diff --git a/connect/resource/fulfillment_automation.py b/connect/resource/fulfillment_automation.py index dec637a..0e53dfa 100644 --- a/connect/resource/fulfillment_automation.py +++ b/connect/resource/fulfillment_automation.py @@ -8,23 +8,26 @@ from connect.logger import logger from connect.models import ActivationTemplateResponse, ActivationTileResponse from connect.models.exception import FulfillmentFail, FulfillmentInquire, Skip +from connect.models.fulfillment import Fulfillment from .fulfillment import FulfillmentResource class FulfillmentAutomation(FulfillmentResource): def process(self): - for _ in self.list(): + # type: () -> None + for _ in self.list: self.dispatch(_) def dispatch(self, request): + # type: (Fulfillment) -> str try: logger.info('Start request process / ID request - {}'.format(request.id)) result = self.process_request(request) if not result: logger.info('Method `process_request` did not return result') - return + return '' params = {} if isinstance(result, ActivationTileResponse): @@ -43,8 +46,7 @@ def dispatch(self, request): except Skip as skip: return skip.code - return def process_request(self, request): - raise NotImplementedError( - 'Please implementation `process_request` logic') + # type: (Fulfillment) -> str + raise NotImplementedError('Please implement `process_request` logic') diff --git a/connect/resource/template.py b/connect/resource/template.py index 985aa75..78a065c 100644 --- a/connect/resource/template.py +++ b/connect/resource/template.py @@ -4,6 +4,7 @@ This file is part of the Ingram Micro Cloud Blue Connect SDK. Copyright (c) 2019 Ingram Micro. All Rights Reserved. """ +from typing import List, Any from connect.models import ActivationTemplateResponse, ActivationTileResponse from .base import BaseResource @@ -17,7 +18,13 @@ class TemplateResource(BaseResource): """ resource = 'templates' + @property + def list(self): + # type: () -> List[Any] + raise AttributeError('This resource do not have method `list`') + def render(self, pk, request_id): + # type: (str, str) -> ActivationTileResponse if not all([pk, request_id]): raise ValueError('Invalid ids for render template') @@ -27,7 +34,5 @@ def render(self, pk, request_id): return ActivationTileResponse(response) def get(self, pk): + # type: (str) -> ActivationTemplateResponse return ActivationTemplateResponse(template_id=pk) - - def list(self): - raise AttributeError('This resource do not have method `list`') diff --git a/connect/resource/utils.py b/connect/resource/utils.py index aab6960..947fead 100644 --- a/connect/resource/utils.py +++ b/connect/resource/utils.py @@ -9,6 +9,7 @@ def join_url(base, url, allow_fragments=True): + # type: (str, str, bool) -> str """ Method for the correct formation of the URL """ if base and isinstance(base, str): base += '/' if base[-1] != '/' else '' diff --git a/example/example.py b/example/example.py index f5cc439..bf72107 100644 --- a/example/example.py +++ b/example/example.py @@ -4,12 +4,14 @@ This file is part of the Ingram Micro Cloud Blue Connect SDK. Copyright (c) 2019 Ingram Micro. All Rights Reserved. """ +from typing import Union from connect import FulfillmentAutomation from connect.config import Config from connect.logger import logger from connect.models import ActivationTemplateResponse, ActivationTileResponse from connect.models.exception import FulfillmentFail, FulfillmentInquire, Skip +from connect.models.fulfillment import Fulfillment # Set logger level / default level ERROR logger.setLevel("DEBUG") @@ -20,6 +22,7 @@ class ExampleRequestProcessor(FulfillmentAutomation): def process_request(self, request): + # type: (Fulfillment) -> Union[ActivationTemplateResponse, ActivationTileResponse] logger.info('Processing request {} for contract {}, product {}, marketplace {}' .format(request.id, diff --git a/tests/test_config.py b/tests/test_config.py index c4fd4fe..bd09ad3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -37,6 +37,7 @@ def test_global_implicit_global_config(): assert Config.get_instance().products[0] == conf_dict.get('products') +# noinspection PyPropertyAccess def test_global_config_immutable_properties(): with pytest.raises(AttributeError): Config.get_instance().api_key = conf_dict.get('apiKey') diff --git a/tests/test_models.py b/tests/test_models.py index ace6f4f..4299a38 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -40,7 +40,7 @@ def test_create_model_from_response(): # Get requests from response resource = FulfillmentResource() - requests = resource.list() + requests = resource.list request_obj = resource.get(pk='PR-000-000-000') # Assert that all properties exist @@ -86,7 +86,7 @@ def test_create_model_from_response(): @patch('requests.get', MagicMock(return_value=_get_response2_ok())) def test_fulfillment_items(): # Get request - requests = FulfillmentResource().list() + requests = FulfillmentResource().list assert len(requests) == 1 request = requests[0] assert isinstance(request, Fulfillment) @@ -116,7 +116,7 @@ def test_fulfillment_items(): @patch('requests.get', MagicMock(return_value=_get_response2_ok())) def test_asset_methods(): # Get asset - requests = FulfillmentResource().list() + requests = FulfillmentResource().list assert len(requests) == 1 assert isinstance(requests[0], Fulfillment) asset = requests[0].asset