예제 #1
0
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"]
예제 #2
0
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}
예제 #3
0
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
예제 #4
0
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}")
예제 #5
0
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
예제 #6
0
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}
예제 #7
0
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"
예제 #8
0
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
예제 #9
0
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
예제 #10
0
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())]
예제 #11
0
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)
예제 #12
0
@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)
예제 #13
0
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
    }
예제 #14
0
def resync(subscription: SubscriptionModel) -> State:
    """Transition a subscription to in sync."""
    subscription.insync = True
    return {"subscription": subscription}
예제 #15
0
 def _set_status(subscription: SubscriptionModel) -> State:
     """Set subscription to status."""
     subscription = SubscriptionModel.from_other_lifecycle(
         subscription, status)
     return {"subscription": subscription}
예제 #16
0
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}