diff --git a/backend/moonstream/admin/__init__.py b/backend/moonstream/admin/__init__.py new file mode 100644 index 00000000..95b6df43 --- /dev/null +++ b/backend/moonstream/admin/__init__.py @@ -0,0 +1,3 @@ +""" +Administrative utilities for operating a Moonstream API deployment. +""" diff --git a/backend/moonstream/admin/cli.py b/backend/moonstream/admin/cli.py index a21dc10c..51df0c91 100644 --- a/backend/moonstream/admin/cli.py +++ b/backend/moonstream/admin/cli.py @@ -2,138 +2,143 @@ Moonstream CLI """ import argparse +from typing import Optional -from bugout.data import BugoutResources -from bugout.exceptions import BugoutResponseException - -from ..settings import ( - MOONSTREAM_ADMIN_ACCESS_TOKEN, - MOONSTREAM_APPLICATION_ID, - bugout_client as bc, -) +from . import subscription_types -class BroodResourcesInteractionException(Exception): - pass +def parse_boolean_arg(raw_arg: Optional[str]) -> Optional[bool]: + if raw_arg is None: + return None - -class UnExpectedException(Exception): - pass - - -def add_subscription_handler(args: argparse.Namespace) -> None: - """ - Handler for "groups subscription add" subcommand. - """ - new_subscription_id = 0 - params = {"type": "subscription_type"} - - try: - # resolve index - try: - resources: BugoutResources = bc.list_resources( - token=MOONSTREAM_ADMIN_ACCESS_TOKEN, params=params - ) - new_subscription_id = ( - max( - [ - int(resource.resource_data["id"]) - for resource in resources.resources - ] - ) - + 1 - ) - except BugoutResponseException as e: - if e.detail != "Resources not found": - raise BroodResourcesInteractionException( - f"status_code={e.status_code}, detail={e.detail}" - ) - # If Brood returns 404, then we want to continue execution of the outer try block - # with new_subscription_id as 0. That's why we don't have an "else" condition here. - except Exception as e: - print("Unexpected Exception on request to brood") - raise - - subscription_data = { - "type": "subscription_type", - "id": str(new_subscription_id), - "name": args.name, - "description": args.description, - "stripe_product_id": args.stripe_product_id, - "stripe_price_id": args.stripe_price_id, - "active": args.active, - } - - try: - bc.create_resource( - token=MOONSTREAM_ADMIN_ACCESS_TOKEN, - application_id=MOONSTREAM_APPLICATION_ID, - resource_data=subscription_data, - ) - except BugoutResponseException as e: - print(f"status_code={e.status_code}, detail={e.detail}") - raise BroodResourcesInteractionException( - f"status_code={e.status_code}, detail={e.detail}" - ) - except Exception as e: - print(f"Exception in create brood resource error:{e}") - raise UnExpectedException("Error in resource creating") - - except Exception as e: - print(e) + raw_arg_lower = raw_arg.lower() + if raw_arg_lower in ["t", "true", "1", "y", "yes"]: + return True + return False def main() -> None: - parser = argparse.ArgumentParser(description="Moonstream CLI") + parser = argparse.ArgumentParser(description="Moonstream Admin CLI") parser.set_defaults(func=lambda _: parser.print_help()) subcommands = parser.add_subparsers(description="Moonstream commands") - parser_subscription = subcommands.add_parser( - "subscription-type", description="Manage Moonstream subscription types" + parser_subscription_types = subcommands.add_parser( + "subtypes", description="Manage Moonstream subscription types" ) - parser_subscription.set_defaults(func=lambda _: parser_subscription.print_help()) - subcommands_subscription = parser_subscription.add_subparsers( - description="Moonstream subscription commands" + parser_subscription_types.set_defaults( + func=lambda _: parser_subscription_types.print_help() ) + subcommands_subscription_types = parser_subscription_types.add_subparsers() - # Subscriptions command parser - parser_subscription_create = subcommands_subscription.add_parser( - "create", description="Create Moonstream subscription" + parser_subscription_types_create = subcommands_subscription_types.add_parser( + "create", description="Create subscription type" ) - parser_subscription_create.add_argument( + parser_subscription_types_create.add_argument( + "-i", "--id", required=True, type=str, help="ID for the subscription type" + ) + parser_subscription_types_create.add_argument( "-n", "--name", required=True, type=str, - help="Title of that subscription", + help="Human-friendly name for the subscription type", ) - parser_subscription_create.add_argument( + parser_subscription_types_create.add_argument( "-d", "--description", required=True, type=str, - help="Description for user", + help="Detailed description of the subscription type", ) - parser_subscription_create.add_argument( + parser_subscription_types_create.add_argument( "--stripe-product-id", required=False, default=None, type=str, help="Stripe product id", ) - parser_subscription_create.add_argument( + parser_subscription_types_create.add_argument( "--stripe-price-id", required=False, default=None, type=str, help="Stripe price id", ) - parser_subscription_create.add_argument( + parser_subscription_types_create.add_argument( "--active", action="store_true", - help="Set this flag to create a verified user", + help="Set this flag to mark the subscription as active", + ) + parser_subscription_types_create.set_defaults( + func=subscription_types.cli_add_subscription_type + ) + + parser_subscription_types_list = subcommands_subscription_types.add_parser( + "list", description="List subscription types" + ) + parser_subscription_types_list.set_defaults( + func=subscription_types.cli_list_subscription_types + ) + + parser_subscription_types_get = subcommands_subscription_types.add_parser( + "get", description="Get a subscription type by its ID" + ) + parser_subscription_types_get.add_argument( + "-i", + "--id", + required=True, + help="ID of the subscription type you would like information about", + ) + parser_subscription_types_get.set_defaults( + func=subscription_types.cli_get_subscription_type + ) + + parser_subscription_types_update = subcommands_subscription_types.add_parser( + "update", description="Create subscription type" + ) + parser_subscription_types_update.add_argument( + "-i", "--id", required=True, type=str, help="ID for the subscription type" + ) + parser_subscription_types_update.add_argument( + "-n", + "--name", + required=False, + default=None, + type=str, + help="Human-friendly name for the subscription type", + ) + parser_subscription_types_update.add_argument( + "-d", + "--description", + required=False, + default=None, + type=str, + help="Detailed description of the subscription type", + ) + parser_subscription_types_update.add_argument( + "--stripe-product-id", + required=False, + default=None, + type=str, + help="Stripe product id", + ) + parser_subscription_types_update.add_argument( + "--stripe-price-id", + required=False, + default=None, + type=str, + help="Stripe price id", + ) + parser_subscription_types_update.add_argument( + "--active", + required=False, + type=parse_boolean_arg, + default=None, + help="Mark the subscription as active (True) or inactive (False).", + ) + parser_subscription_types_update.set_defaults( + func=subscription_types.cli_update_subscription_type ) - parser_subscription_create.set_defaults(func=add_subscription_handler) args = parser.parse_args() args.func(args) diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py new file mode 100644 index 00000000..8e3ab5ad --- /dev/null +++ b/backend/moonstream/admin/subscription_types.py @@ -0,0 +1,231 @@ +""" +Utilities for managing subscription type resources for a Moonstream application. +""" +import argparse +import json +from typing import Any, Dict, List, Optional +from bugout.app import Bugout + +from bugout.data import BugoutResources, BugoutResource + +from ..settings import ( + MOONSTREAM_ADMIN_ACCESS_TOKEN, + MOONSTREAM_APPLICATION_ID, + bugout_client as bc, +) + + +class ConflictingSubscriptionTypesError(Exception): + """ + Raised when caller tries to add a resource that conflicts with an existing resource. + """ + + pass + + +class SubscriptionTypeNotFoundError(Exception): + """ + Raised when a subscription type is expected to exist as a Brood resource but is not found. + """ + + +class UnexpectedError(Exception): + pass + + +BUGOUT_RESOURCE_TYPE = "subscription_type" + + +def add_subscription_type( + id: str, + name: str, + description: str, + stripe_product_id: Optional[str] = None, + stripe_price_id: Optional[str] = None, + active: bool = False, +) -> Dict[str, Any]: + """ + Add a new Moonstream subscription type as a Brood resource. + + Args: + - id: Moonstream ID for the subscription type. Examples: "ethereum_blockchain", "ethereum_txpool", + "ethereum_whalewatch", etc. + - name: Human-friendly name for the subscription type, which can be displayed to users. + - description: Detailed description of the subscription type for users who would like more + information. + - stripe_product_id: Optional product ID from Stripe account dashboard. + - stripe_price_id: Optional price ID from Stripe account dashboard. + - active: Set to True if you would like the subscription type to immediately be available for + subscriptions. If you set this to False (which is the default), users will not be able to create + subscriptions of this type until you later on set to true. + """ + params = {"type": BUGOUT_RESOURCE_TYPE, "id": id} + + response: BugoutResources = bc.list_resources( + token=MOONSTREAM_ADMIN_ACCESS_TOKEN, params=params + ) + if response.resources: + raise ConflictingSubscriptionTypesError( + f"There is already a subscription_type with id: {id}" + ) + + subscription_data = { + "type": BUGOUT_RESOURCE_TYPE, + "id": id, + "name": name, + "description": description, + "stripe_product_id": stripe_product_id, + "stripe_price_id": stripe_price_id, + "active": active, + } + + resource = bc.create_resource( + token=MOONSTREAM_ADMIN_ACCESS_TOKEN, + application_id=MOONSTREAM_APPLICATION_ID, + resource_data=subscription_data, + ) + + return resource.resource_data + + +def cli_add_subscription_type(args: argparse.Namespace) -> None: + """ + Handler for "mnstr subtypes create". + """ + result = add_subscription_type( + args.id, + args.name, + args.description, + args.stripe_product_id, + args.stripe_price_id, + args.active, + ) + print(json.dumps(result)) + + +def list_subscription_types() -> List[Dict[str, Any]]: + """ + Lists all subscription types registered as Brood resources for this Moonstream application. + """ + response = bc.list_resources( + token=MOONSTREAM_ADMIN_ACCESS_TOKEN, params={"type": BUGOUT_RESOURCE_TYPE} + ) + resources = response.resources + return [resource.resource_data for resource in resources] + + +def cli_list_subscription_types(args: argparse.Namespace) -> None: + """ + Handler for "mnstr subtypes list". + """ + results = list_subscription_types() + print(json.dumps(results)) + + +def get_subscription_type(id: str) -> Optional[BugoutResource]: + """ + Retrieves the resource representing the subscription type with the given ID. + + Args: + - id: Moonstream ID for the subscription type (not the Brood resource ID). + Examples - "ethereum_blockchain", "ethereum_whalewatch", etc. + + Returns: None if there is no subscription type with that ID. Otherwise, returns the full + Brood resource. To access the subscription type itself, use the "resource_data" member of the + return value. If more than one subscription type is found with the given ID, raises a + ConflictingSubscriptionTypesError. + """ + response = bc.list_resources( + token=MOONSTREAM_ADMIN_ACCESS_TOKEN, + params={"type": BUGOUT_RESOURCE_TYPE, "id": id}, + ) + resources = response.resources + + if not resources: + return None + if len(resources) > 1: + raise ConflictingSubscriptionTypesError( + f"More than one resource with the given ID:\n{json.dumps(resources, indent=2)}" + ) + return resources[0] + + +def cli_get_subscription_type(args: argparse.Namespace) -> None: + """ + Handler for "mnstr subtypes get". + """ + resource = get_subscription_type(args.id) + if resource is None: + print(f"Could not find resource with ID: {id}") + else: + print(resource.json()) + + +def update_subscription_type( + id: str, + name: Optional[str] = None, + description: Optional[str] = None, + stripe_product_id: Optional[str] = None, + stripe_price_id: Optional[str] = None, + active: Optional[bool] = None, +) -> Dict[str, Any]: + """ + Update a Moonstream subscription type using the Brood Resources API. + + Args: + - id: Moonstream ID for the subscription type. Examples: "ethereum_blockchain", "ethereum_txpool", + "ethereum_whalewatch", etc. + - name: Human-friendly name for the subscription type, which can be displayed to users. + - description: Detailed description of the subscription type for users who would like more + information. + - stripe_product_id: Optional product ID from Stripe account dashboard. + - stripe_price_id: Optional price ID from Stripe account dashboard. + - active: Set to True if you would like the subscription type to immediately be available for + subscriptions. If you set this to False (which is the default), users will not be able to create + subscriptions of this type until you later on set to true. + """ + + resource = get_subscription_type(id) + if resource is None: + raise SubscriptionTypeNotFoundError( + f"Could not find subscription type with ID: {id}." + ) + + brood_resource_id = resource.id + updated_resource_data = resource.resource_data + if name is not None: + updated_resource_data["name"] = name + if description is not None: + updated_resource_data["description"] = description + if stripe_product_id is not None: + updated_resource_data["stripe_product_id"] = stripe_product_id + if stripe_price_id is not None: + updated_resource_data["stripe_price_id"] = stripe_price_id + if active is not None: + updated_resource_data["active"] = active + + bc.delete_resource( + token=MOONSTREAM_ADMIN_ACCESS_TOKEN, resource_id=brood_resource_id + ) + new_resource = bc.create_resource( + token=MOONSTREAM_ADMIN_ACCESS_TOKEN, + application_id=MOONSTREAM_APPLICATION_ID, + resource_data=updated_resource_data, + ) + + return new_resource.resource_data + + +def cli_update_subscription_type(args: argparse.Namespace) -> None: + """ + Handler for "mnstr subtypes update". + """ + result = update_subscription_type( + args.id, + args.name, + args.description, + args.stripe_product_id, + args.stripe_price_id, + args.active, + ) + print(json.dumps(result)) diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py index 68342e17..c48c180f 100644 --- a/backend/moonstream/data.py +++ b/backend/moonstream/data.py @@ -10,12 +10,13 @@ class SubscriptionTypeResourceData(BaseModel): id: str name: str description: str - subscription_plan_id: Optional[str] = None + stripe_product_id: Optional[str] = None + stripe_price_id: Optional[str] = None active: bool = False class SubscriptionTypesListResponce(BaseModel): - subscriptions: List[SubscriptionTypeResourceData] = Field(default_factory=list) + subscription_types: List[SubscriptionTypeResourceData] = Field(default_factory=list) class SubscriptionResourceData(BaseModel): diff --git a/backend/moonstream/version.py b/backend/moonstream/version.py index f899a8eb..20be0ba5 100644 --- a/backend/moonstream/version.py +++ b/backend/moonstream/version.py @@ -2,4 +2,4 @@ Moonstream library and API version. """ -MOONSTREAM_VERSION = "0.0.1" +MOONSTREAM_VERSION = "0.0.2" diff --git a/backend/setup.py b/backend/setup.py index 38230579..e9bf2d77 100644 --- a/backend/setup.py +++ b/backend/setup.py @@ -29,4 +29,5 @@ setup( "Topic :: Software Development :: Libraries", ], url="https://github.com/bugout-dev/moonstream", + entry_points={"console_scripts": ["mnstr=moonstream.admin.cli:main"]}, )