def test_product_block_paths(generic_subscription_1, generic_subscription_2): subscription_1 = SubscriptionModel.from_subscription( generic_subscription_1) subscription_2 = SubscriptionModel.from_subscription( generic_subscription_2) assert product_block_paths(subscription_1) == ["product", "pb_1", "pb_2"] assert product_block_paths(subscription_2) == ["product", "pb_3"]
def check_subscription_models() -> State: subscriptions = SubscriptionTable.query.all() failures: Dict[str, JSON] = {} for subscription in subscriptions: try: SubscriptionModel.from_subscription(subscription.subscription_id) except ValidationError as e: failures[str(subscription.subscription_id)] = e.errors() except Exception as e: failures[str(subscription.subscription_id)] = str(e) if failures: raise ProcessFailure("Found subscriptions that could not be loaded", failures) return {"check_subscription_models": True}
def test_push_subscriptions_to_cache_step(generic_subscription_1): push_subscription = orig(cache_domain_models) push_subscription( "Aribtrary_name", SubscriptionModel.from_subscription(generic_subscription_1)) cache = Redis(host=app_settings.CACHE_HOST, port=app_settings.CACHE_PORT) assert cache.get(f"domain:{generic_subscription_1}") is None app_settings.CACHE_DOMAIN_MODELS = True push_subscription( "Aribtrary_name", SubscriptionModel.from_subscription(generic_subscription_1)) result = cache.get(f"domain:{generic_subscription_1}") assert result is not None app_settings.CACHE_DOMAIN_MODELS = False
def test_subscription_detail_with_domain_model_cache(test_client, generic_subscription_1): # test with a subscription that has domain model and without subscription = SubscriptionModel.from_subscription(generic_subscription_1) extended_model = build_extendend_domain_model(subscription) etag = _generate_etag(extended_model) app_settings.CACHE_DOMAIN_MODELS = True to_redis(extended_model) response = test_client.get( URL("api/subscriptions/domain-model") / generic_subscription_1) cache = Redis(host=app_settings.CACHE_HOST, port=app_settings.CACHE_PORT) result = cache.get(f"domain:{generic_subscription_1}") cached_model = json_dumps(json_loads(result)) cached_etag = cache.get(f"domain:etag:{generic_subscription_1}") assert cached_model == json_dumps(extended_model) assert cached_etag.decode("utf-8") == etag assert response.status_code == HTTPStatus.OK assert response.json()["subscription_id"] == generic_subscription_1 app_settings.CACHE_DOMAIN_MODELS = False cache.delete(f"domain:{generic_subscription_1}")
def test_subscription_detail_with_domain_model_if_none_match( test_client, generic_subscription_1): # test with a subscription that has domain model and without subscription = SubscriptionModel.from_subscription(generic_subscription_1) extended_model = build_extendend_domain_model(subscription) etag = _generate_etag(extended_model) response = test_client.get(URL("api/subscriptions/domain-model") / generic_subscription_1, headers={"If-None-Match": etag}) assert response.status_code == HTTPStatus.NOT_MODIFIED
def cache_domain_models( workflow_name: str, subscription: Optional[SubscriptionModel] = None) -> State: """ Attempt to cache all Subscriptions once they have been touched once. Args: workflow_name: The Workflow Name subscription: The Subscription if it exists. Returns: Returns State. """ cached_subscription_ids: Set[UUID] = set() if not subscription: logger.warning("No subscription found in this workflow", workflow_name=workflow_name) return {"cached_subscription_ids": cached_subscription_ids} def _cache_other_subscriptions(product_block: ProductBlockModel) -> None: for field in product_block.__fields__: # subscription_instance is a ProductBlockModel or an arbitrary type subscription_instance: Union[ProductBlockModel, Any] = getattr(product_block, field) # If subscription_instance is a list, we need to step into it and loop. if isinstance(subscription_instance, list): for item in subscription_instance: if isinstance(item, ProductBlockModel): _cache_other_subscriptions(item) # If subscription_instance is a ProductBlockModel check the owner_subscription_id to decide the cache elif isinstance(subscription_instance, ProductBlockModel): _cache_other_subscriptions(subscription_instance) if not subscription_instance.owner_subscription_id == subscription.subscription_id: # type: ignore cached_subscription_ids.add( subscription_instance.owner_subscription_id) for field in subscription.__fields__: # There always is a single Root Product Block, it cannot be a list, so no need to check. instance: Union[ProductBlockModel, Any] = getattr(subscription, field) if isinstance(instance, ProductBlockModel): _cache_other_subscriptions(instance) # Cache all the sub subscriptions for subscription_id in cached_subscription_ids: subscription_model = SubscriptionModel.from_subscription( subscription_id) to_redis(build_extendend_domain_model(subscription_model)) # Cache the main subscription to_redis(build_extendend_domain_model(subscription)) cached_subscription_ids.add(subscription.subscription_id) return {"cached_subscription_ids": cached_subscription_ids}
def test_subscription_detail_with_domain_model_etag(test_client, generic_subscription_1): # test with a subscription that has domain model and without response = test_client.get( URL("api/subscriptions/domain-model") / generic_subscription_1) assert response.status_code == HTTPStatus.OK subscription = SubscriptionModel.from_subscription(generic_subscription_1) extended_model = build_extendend_domain_model(subscription) etag = _generate_etag(extended_model) assert etag == response.headers["ETag"] # Check hierarchy assert response.json()["pb_1"]["rt_1"] == "Value1"
def test_inject_args_optional(generic_product_1, generic_product_type_1) -> None: GenericProductOneInactive, GenericProduct = generic_product_type_1 product_id = generic_product_1.product_id state = {"product": product_id, "organisation": uuid4()} generic_sub = GenericProductOneInactive.from_product_id( product_id=state["product"], customer_id=state["organisation"], status=SubscriptionLifecycle.INITIAL) generic_sub.pb_1.rt_1 = "test" generic_sub.pb_2.rt_2 = 42 generic_sub.pb_2.rt_3 = "test2" generic_sub = SubscriptionModel.from_other_lifecycle( generic_sub, SubscriptionLifecycle.ACTIVE) generic_sub.save() @inject_args def step_existing(generic_sub: Optional[GenericProduct]) -> State: assert generic_sub is not None, "Generic Sub IS NONE" assert generic_sub.subscription_id assert generic_sub.pb_1.rt_1 == "test" return {"generic_sub": generic_sub} with pytest.raises(AssertionError) as exc_info: step_existing(state) assert "Generic Sub IS NONE" in str(exc_info.value) # Put `light_path` as an UUID in. Entire `light_path` object would have worked as well, but this way we will be # certain that if we end up with an entire `light_path` object in the step function, it will have been retrieved # from the database. state["generic_sub"] = generic_sub.subscription_id state_amended = step_existing(state) assert "generic_sub" in state_amended # Do we now have an entire object instead of merely a UUID assert isinstance(state_amended["generic_sub"], GenericProduct) # And does it have the modifcations from the step functions assert state_amended["generic_sub"].pb_1.rt_1 is not None # Test `nso_service_id` has been persisted to the database with the modifications from the step function.` fresh_generic_sub = GenericProduct.from_subscription( state_amended["generic_sub"].subscription_id) assert fresh_generic_sub.pb_1.rt_1 is not None
def test_form_inject_args(generic_product_1, generic_product_type_1) -> None: GenericProductOneInactive, GenericProduct = generic_product_type_1 product_id = generic_product_1.product_id state = {"product": product_id, "organisation": uuid4()} generic_sub = GenericProductOneInactive.from_product_id( product_id=state["product"], customer_id=state["organisation"], status=SubscriptionLifecycle.INITIAL) generic_sub.pb_1.rt_1 = "test" generic_sub.pb_2.rt_2 = 42 generic_sub.pb_2.rt_3 = "test2" generic_sub = SubscriptionModel.from_other_lifecycle( generic_sub, SubscriptionLifecycle.ACTIVE) generic_sub.save() @form_inject_args def form_function(generic_sub: GenericProduct) -> State: assert generic_sub.subscription_id assert generic_sub.pb_1.rt_1 == "test" generic_sub.pb_1.rt_1 = "test string" class Form(FormPage): pass _ = yield Form return {"generic_sub": generic_sub} # Put `generic_sub` as an UUID in. Entire `generic_sub` object would have worked as well, but this way we will be # certain that if we end up with an entire `generic_sub` object in the step function, it will have been retrieved # from the database. state["generic_sub"] = generic_sub.subscription_id state_amended = post_process(form_function, state, [{}]) assert "generic_sub" in state_amended # Do we now have an entire object instead of merely a UUID assert isinstance(state_amended["generic_sub"], GenericProduct) # And does it have the modifcations from the step functions assert state_amended["generic_sub"].pb_1.rt_1 == "test string" # Test `rt_1` has been persisted to the database with the modifications from the step function.` fresh_generic_sub = GenericProduct.from_subscription( state_amended["generic_sub"].subscription_id) assert fresh_generic_sub.pb_1.rt_1 is not None
def product_block_paths( subscription: SubscriptionModel) -> List[Optional[str]]: def get_dict_items(d: dict) -> Generator: for k, v in d.items(): if isinstance(v, dict): for k1, v1 in get_dict_items(v): yield (f"{k}.{k1}", v1) yield (k, v) if isinstance(v, list): for index, list_item in enumerate(v): if isinstance(list_item, dict): for list_item_key, list_item_value in get_dict_items( list_item): yield (f"{k}.{index}.{list_item_key}", list_item_value) yield (f"{k}.{index}", list_item) return [c[0] for c in get_dict_items(subscription.dict())]
def build_extendend_domain_model(subscription_model: SubscriptionModel) -> dict: customer_descriptions = SubscriptionCustomerDescriptionTable.query.filter( SubscriptionCustomerDescriptionTable.subscription_id == subscription_model.subscription_id ).all() subscription = subscription_model.dict() sub_instance_ids = [subscription_model.subscription_id] paths = product_block_paths(subscription_model) for path in paths: sub_instance_ids.append(getattr_in(subscription_model, f"{path}.subscription_instance_id")) # find all product blocks, check if they have in_use_by and inject the in_use_by_ids into the subscription dict. for path in paths: if in_use_by_subs := getattr_in(subscription_model, f"{path}.in_use_by"): i_ids: List[Optional[UUID]] = [] i_ids.extend( in_use_by_subs.col[idx].in_use_by_id for idx, ob in enumerate(in_use_by_subs.col) if ob.in_use_by_id not in sub_instance_ids ) update_in(subscription, f"{path}.in_use_by_ids", i_ids)
@router.get("/domain-model/{subscription_id}", response_model=SubscriptionDomainModelSchema) def subscription_details_by_id_with_domain_model( request: Request, subscription_id: UUID, response: Response ) -> Optional[Dict[str, Any]]: def _build_response(model: dict, etag: str) -> Optional[Dict[str, Any]]: if etag == request.headers.get("If-None-Match"): response.status_code = HTTPStatus.NOT_MODIFIED return None response.headers["ETag"] = etag return model if cache_response := from_redis(subscription_id): return _build_response(*cache_response) try: subscription_model = SubscriptionModel.from_subscription(subscription_id) extended_model = build_extendend_domain_model(subscription_model) etag = _generate_etag(extended_model) return _build_response(extended_model, etag) except ValueError as e: if str(e) == f"Subscription with id: {subscription_id}, does not exist": raise_status(HTTPStatus.NOT_FOUND, f"Subscription with id: {subscription_id}, not found") else: raise_status(HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) @router.delete("/{subscription_id}", response_model=None) def delete_subscription(subscription_id: UUID) -> None: all_process_subscriptions = ProcessSubscriptionTable.query.filter_by(subscription_id=subscription_id).all() if len(all_process_subscriptions) > 0: _delete_process_subscriptions(all_process_subscriptions)
def unsync(subscription_id: UUIDstr, __old_subscriptions__: Optional[dict] = None) -> State: """ Transition a subscription to out of sync. This step will also create a backup of the current subscription details in the state with the key `__old_subscriptions__` Note: This step will NOT overwrite any existing data in the state for the `__old_subscriptions__` key. Some workflows already change subscription details in the initial form step. This is not a best practice but it can be handy/needed for some scenarios. To ensure that you get a correct backup these forms need to create the backup in the initial form step and save it to `__old_subscriptions__`. An example, showing the creation of a backup for 2 subscriptions during the initial form step : ``` subscription = YOUR_DOMAIN_MODEL.from_subscription(subscription_id) subscription_backup = {} subscription_backup[subscription.subscription_id] = deepcopy(subscription.dict()) subscription_backup[second_subscription.subscription_id] = deepcopy(second_subscription.dict()) return {..., "__old_subscriptions__": subscription_backup } ``` It's also possible to make the backup for a subscription that doesn't have a domain model: ``` from orchestrator.utils.json import to_serializable subscription = subscriptions.get_subscription(subscription_id) subscription_backup = {subscription_id: to_serializable(subscription)} Do your changes here ... return {..., "subscription": to_serializable(subscription), "__old_subscriptions__": subscription_backup } ``` """ try: subscription = SubscriptionModel.from_subscription(subscription_id) except ValidationError: subscription = get_subscription(subscription_id) # type: ignore # Handle backup if needed if __old_subscriptions__ and __old_subscriptions__.get(subscription_id): logger.info( "Skipping backup of subscription details because it already exists in the state", subscription_id=str(subscription_id), ) subscription_backup = __old_subscriptions__ else: logger.info("Creating backup of subscription details in the state", subscription_id=str(subscription_id)) subscription_backup = __old_subscriptions__ or {} if isinstance(subscription, SubscriptionModel): subscription_backup[str(subscription_id)] = deepcopy( subscription.dict()) else: subscription_backup[str(subscription_id)] = to_serializable( subscription) # type: ignore # Handle transition if not subscription.insync: raise ValueError( "Subscription is already out of sync, cannot continue!") subscription.insync = False return { "subscription": subscription, "__old_subscriptions__": subscription_backup }
def resync(subscription: SubscriptionModel) -> State: """Transition a subscription to in sync.""" subscription.insync = True return {"subscription": subscription}
def _set_status(subscription: SubscriptionModel) -> State: """Set subscription to status.""" subscription = SubscriptionModel.from_other_lifecycle( subscription, status) return {"subscription": subscription}
def unsync_unchecked(subscription_id: UUIDstr) -> State: """Use for validation workflows that need to run if the subscription is out of sync.""" subscription = SubscriptionModel.from_subscription(subscription_id) subscription.insync = False return {"subscription": subscription}