From 2485e60742e31ce21958f6791165af86c51f2d95 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:09:33 +0000 Subject: [PATCH 01/19] subscribe decorator --- brewtils/decorators.py | 83 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/brewtils/decorators.py b/brewtils/decorators.py index 433984f9..81cdc085 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -408,7 +408,7 @@ def cmd1(self, **kwargs): return _wrapped -def subscribe(_wrapped=None, topic: str = None, topics=[]): +def subscribe(_wrapped=None, topic: str = None, topics=[], dynamic_topics: dict=None): """Decorator for specifiying topic to listen to. for example:: @@ -427,6 +427,7 @@ def returnTrue(self): shouldn't be explicitly set. topic: The topic to subscribe to topics: A list of topics to subscribe to + dynamic_topics: Dictionary containing dynamic topics specification """ subscribe_topics = [] @@ -438,6 +439,86 @@ def returnTrue(self): if list_topic not in subscribe_topics: subscribe_topics.append(list_topic) + if callable(dynamic_topics): + dynamic_topics = dynamic_topics() + for dynamic_topic in dynamic_topics: + if dynamic_topic not in subscribe_topics: + subscribe_topics.append(dynamic_topic) + + if isinstance(dynamic_topics, dict): + if not dynamic_topics.get("value"): + raise PluginParamError( + "No 'value' provided for topics. You must at least " + "provide valid values." + ) + + # Again, if value is a Callable, call it + value = dynamic_topics.get("value") + if callable(value): + value = value() + + # Determine type of value + topic_type = dynamic_topics.get("type") + + # TODO + topic_types = ["static", "url", "command"] + if topic_type not in topic_types: + raise PluginParamError( + "Invalid topic type '%s' - Valid type options are %s" + % (topic_type, topic_types) + ) + else: + if ( + ( + topic_type == "command" + and not isinstance(value, (six.string_types, dict)) + ) + or (topic_type == "url" and not isinstance(value, six.string_types)) + or (topic_type == "static" and not isinstance(value, (list, dict))) + ): + allowed_types = { + "command": "('string', 'dictionary')", + "url": "('string')", + "static": "('list', 'dictionary)", + } + raise PluginParamError( + "Invalid topics value type '%s' - Valid value types for " + "topic type '%s' are %s" + % (type(value), topic_type, allowed_types[topic_type]) + ) + + # TODO: Is this needed? + # Now parse out type-specific aspects + unparsed_value = "" + try: + if choice_type == "command": + if isinstance(value, six.string_types): + unparsed_value = value + else: + unparsed_value = value["command"] + + details = parse(unparsed_value, parse_as="func") + elif choice_type == "url": + unparsed_value = value + details = parse(unparsed_value, parse_as="url") + else: + if isinstance(value, dict): + unparsed_value = choices.get("key_reference") + if unparsed_value is None: + raise PluginParamError( + "Specifying a static choices dictionary requires a " + '"key_reference" field with a reference to another ' + 'parameter ("key_reference": "${param_key}")' + ) + + details = {"key_reference": parse(unparsed_value, parse_as="reference")} + else: + details = {} + except ParseError: + raise PluginParamError( + "Invalid choices definition - Unable to parse '%s'" % unparsed_value + ) + if _wrapped is None: return functools.partial(subscribe, topics=subscribe_topics) From ffa611c1680170d77959970a786cffa7bbc2b0d7 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:51:59 +0000 Subject: [PATCH 02/19] revert subscribe --- brewtils/decorators.py | 83 +----------------------------------------- 1 file changed, 1 insertion(+), 82 deletions(-) diff --git a/brewtils/decorators.py b/brewtils/decorators.py index 81cdc085..433984f9 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -408,7 +408,7 @@ def cmd1(self, **kwargs): return _wrapped -def subscribe(_wrapped=None, topic: str = None, topics=[], dynamic_topics: dict=None): +def subscribe(_wrapped=None, topic: str = None, topics=[]): """Decorator for specifiying topic to listen to. for example:: @@ -427,7 +427,6 @@ def returnTrue(self): shouldn't be explicitly set. topic: The topic to subscribe to topics: A list of topics to subscribe to - dynamic_topics: Dictionary containing dynamic topics specification """ subscribe_topics = [] @@ -439,86 +438,6 @@ def returnTrue(self): if list_topic not in subscribe_topics: subscribe_topics.append(list_topic) - if callable(dynamic_topics): - dynamic_topics = dynamic_topics() - for dynamic_topic in dynamic_topics: - if dynamic_topic not in subscribe_topics: - subscribe_topics.append(dynamic_topic) - - if isinstance(dynamic_topics, dict): - if not dynamic_topics.get("value"): - raise PluginParamError( - "No 'value' provided for topics. You must at least " - "provide valid values." - ) - - # Again, if value is a Callable, call it - value = dynamic_topics.get("value") - if callable(value): - value = value() - - # Determine type of value - topic_type = dynamic_topics.get("type") - - # TODO - topic_types = ["static", "url", "command"] - if topic_type not in topic_types: - raise PluginParamError( - "Invalid topic type '%s' - Valid type options are %s" - % (topic_type, topic_types) - ) - else: - if ( - ( - topic_type == "command" - and not isinstance(value, (six.string_types, dict)) - ) - or (topic_type == "url" and not isinstance(value, six.string_types)) - or (topic_type == "static" and not isinstance(value, (list, dict))) - ): - allowed_types = { - "command": "('string', 'dictionary')", - "url": "('string')", - "static": "('list', 'dictionary)", - } - raise PluginParamError( - "Invalid topics value type '%s' - Valid value types for " - "topic type '%s' are %s" - % (type(value), topic_type, allowed_types[topic_type]) - ) - - # TODO: Is this needed? - # Now parse out type-specific aspects - unparsed_value = "" - try: - if choice_type == "command": - if isinstance(value, six.string_types): - unparsed_value = value - else: - unparsed_value = value["command"] - - details = parse(unparsed_value, parse_as="func") - elif choice_type == "url": - unparsed_value = value - details = parse(unparsed_value, parse_as="url") - else: - if isinstance(value, dict): - unparsed_value = choices.get("key_reference") - if unparsed_value is None: - raise PluginParamError( - "Specifying a static choices dictionary requires a " - '"key_reference" field with a reference to another ' - 'parameter ("key_reference": "${param_key}")' - ) - - details = {"key_reference": parse(unparsed_value, parse_as="reference")} - else: - details = {} - except ParseError: - raise PluginParamError( - "Invalid choices definition - Unable to parse '%s'" % unparsed_value - ) - if _wrapped is None: return functools.partial(subscribe, topics=subscribe_topics) From 700afe743044045229fc939c411345bbb52f7e62 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:06:28 +0000 Subject: [PATCH 03/19] Topic subscriber models --- brewtils/models.py | 54 +++++++++++++++++++++++++++++++++++++++ brewtils/schema_parser.py | 2 ++ brewtils/schemas.py | 18 +++++++++++++ brewtils/test/fixtures.py | 24 +++++++++++++++++ test/models_test.py | 31 ++++++++++++++++++++++ 5 files changed, 129 insertions(+) diff --git a/brewtils/models.py b/brewtils/models.py index c60c794a..9cb7c881 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -37,6 +37,8 @@ "Garden", "Operation", "Resolvable", + "TopicSubscribers", + "Subscriber", ] @@ -1649,3 +1651,55 @@ def __repr__(self): self.storage, self.details, ) + + +class TopicSubscribers: + schema = "TopicSubscribersSchema" + + def __init__( + self, + topic=None, + subscribers=[] + ): + self.topic = topic + self.subscribers = subscribers + + def __str__(self): + return "%s: %s" % (self.topic, [str(s) for s in self.subscribers]) + + def __repr__(self): + return "" % (self.topic, self.subscribers) + + +class Subscriber: + schema = "SubscriberSchema" + + def __init__( + self, + garden=None, + namespace=None, + system=None, + version=None, + instance=None, + command=None + ): + self.garden = garden + self.namespace = namespace + self.system = system + self.version = version + self.instance = instance + self.command = command + + def __str__(self): + return ( + f"{self.garden}.{self.namespace}.{self.system}.{self.version}.{self.instance}." + f"{self.command}" + ) + + def __repr__(self): + return ( + "" % ( + self.garden, self.namespace, self.system, self.version, self.instance, self.command + ) + ) diff --git a/brewtils/schema_parser.py b/brewtils/schema_parser.py index 98598ada..766d5172 100644 --- a/brewtils/schema_parser.py +++ b/brewtils/schema_parser.py @@ -50,6 +50,8 @@ class SchemaParser(object): "OperationSchema": brewtils.models.Operation, "RunnerSchema": brewtils.models.Runner, "ResolvableSchema": brewtils.models.Resolvable, + "SubscriberSchema": brewtils.models.Subscriber, + "TopicSubscribersSchema": brewtils.models.TopicSubscribers, } logger = logging.getLogger(__name__) diff --git a/brewtils/schemas.py b/brewtils/schemas.py index e734f13a..0b484921 100644 --- a/brewtils/schemas.py +++ b/brewtils/schemas.py @@ -46,6 +46,8 @@ "RoleAssignmentDomainSchema", "GardenDomainIdentifierSchema", "SystemDomainIdentifierSchema", + "TopicSubscribersSchema", + "SubscriberSchema" ] # This will be updated after all the schema classes are defined @@ -624,6 +626,20 @@ class RoleAssignmentSchema(BaseSchema): role = fields.Nested(RoleSchema()) +class SubscriberSchema(BaseSchema): + garden = fields.Str(allow_none=True) + namespace = fields.Str(allow_none=True) + system = fields.Str(allow_none=True) + version = fields.Str(allow_none=True) + instance = fields.Str(allow_none=True) + command = fields.Str(allow_none=True) + + +class TopicSubscribersSchema(BaseSchema): + topic = fields.Str(allow_none=True) + subscribers = fields.List(fields.Nested(SubscriberSchema, allow_none=True)) + + class UserSchema(BaseSchema): id = fields.Str() username = fields.Str() @@ -671,6 +687,8 @@ class UserListSchema(BaseSchema): "Operation": OperationSchema, "Runner": RunnerSchema, "Resolvable": ResolvableSchema, + "Subscriber": SubscriberSchema, + "TopicSubscribers": TopicSubscribersSchema, # Compatibility for the Job trigger types "interval": IntervalTriggerSchema, "date": DateTriggerSchema, diff --git a/brewtils/test/fixtures.py b/brewtils/test/fixtures.py index cef700d8..709582e0 100644 --- a/brewtils/test/fixtures.py +++ b/brewtils/test/fixtures.py @@ -30,6 +30,8 @@ Resolvable, Runner, System, + Subscriber, + TopicSubscribers, ) @@ -903,3 +905,25 @@ def resolvable_chunk_dict(): @pytest.fixture def bg_resolvable_chunk(resolvable_chunk_dict): return Resolvable(**resolvable_chunk_dict) + + +@pytest.fixture +def subscriber_dict(): + """Subscribers as a dictionary.""" + return { + "garden": "garden", + "namespace": "ns", + "system": "system", + "version": "1.0.0", + "instance": None, + "command": None + } + + +@pytest.fixture +def topic_subscriber_dict(subscriber_dict): + """Topic subscribers as dict""" + return { + "topic": "foo", + "subscribers": [subscriber_dict] + } diff --git a/test/models_test.py b/test/models_test.py index b64bafb1..c4b590fc 100644 --- a/test/models_test.py +++ b/test/models_test.py @@ -19,6 +19,8 @@ RequestFile, RequestTemplate, LegacyRole, + Subscriber, + TopicSubscribers, ) from pytest_lazyfixture import lazy_fixture @@ -675,3 +677,32 @@ def test_repr(self, bg_resolvable): bg_resolvable.storage, bg_resolvable.details, ) + + +@pytest.fixture +def subscriber1(): + return Subscriber(garden="g", namespace="n", system="s", version="v", instance="i", command="c") + + +@pytest.fixture +def topic_subscribers1(subscriber1): + return TopicSubscribers(topic="foo.*", subscribers=[subscriber1]) + + +class TestSubscriber(object): + def test_str(self, subscriber1): + assert "g.n.s.v.i.c" == str(subscriber1) + + def test_repr(self, subscriber1): + assert "g" in repr(subscriber1) + assert "n" in repr(subscriber1) + assert "s" in repr(subscriber1) + assert "v" in repr(subscriber1) + assert "i" in repr(subscriber1) + assert "c" in repr(subscriber1) + + +class TestTopicSubscribers(): + def test_str(self, topic_subscribers1, subscriber1): + print(str(topic_subscribers1)) + assert str(topic_subscribers1) == "foo.*: ['g.n.s.v.i.c']" From 68bd0d2f4b9af5ae7a88208e3fbeeed91e737b16 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:20:07 +0000 Subject: [PATCH 04/19] formatting models --- brewtils/models.py | 23 ++++++++++++++--------- brewtils/schemas.py | 2 +- brewtils/test/fixtures.py | 7 ++----- test/models_test.py | 6 ++++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/brewtils/models.py b/brewtils/models.py index 9cb7c881..0afed28a 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -1656,11 +1656,7 @@ def __repr__(self): class TopicSubscribers: schema = "TopicSubscribersSchema" - def __init__( - self, - topic=None, - subscribers=[] - ): + def __init__(self, topic=None, subscribers=[]): self.topic = topic self.subscribers = subscribers @@ -1668,7 +1664,10 @@ def __str__(self): return "%s: %s" % (self.topic, [str(s) for s in self.subscribers]) def __repr__(self): - return "" % (self.topic, self.subscribers) + return "" % ( + self.topic, + self.subscribers, + ) class Subscriber: @@ -1681,7 +1680,7 @@ def __init__( system=None, version=None, instance=None, - command=None + command=None, ): self.garden = garden self.namespace = namespace @@ -1699,7 +1698,13 @@ def __str__(self): def __repr__(self): return ( "" % ( - self.garden, self.namespace, self.system, self.version, self.instance, self.command + "command=%s>" + % ( + self.garden, + self.namespace, + self.system, + self.version, + self.instance, + self.command, ) ) diff --git a/brewtils/schemas.py b/brewtils/schemas.py index 0b484921..e4c9b04f 100644 --- a/brewtils/schemas.py +++ b/brewtils/schemas.py @@ -47,7 +47,7 @@ "GardenDomainIdentifierSchema", "SystemDomainIdentifierSchema", "TopicSubscribersSchema", - "SubscriberSchema" + "SubscriberSchema", ] # This will be updated after all the schema classes are defined diff --git a/brewtils/test/fixtures.py b/brewtils/test/fixtures.py index 709582e0..f8ad422c 100644 --- a/brewtils/test/fixtures.py +++ b/brewtils/test/fixtures.py @@ -916,14 +916,11 @@ def subscriber_dict(): "system": "system", "version": "1.0.0", "instance": None, - "command": None + "command": None, } @pytest.fixture def topic_subscriber_dict(subscriber_dict): """Topic subscribers as dict""" - return { - "topic": "foo", - "subscribers": [subscriber_dict] - } + return {"topic": "foo", "subscribers": [subscriber_dict]} diff --git a/test/models_test.py b/test/models_test.py index c4b590fc..3dbb2dfc 100644 --- a/test/models_test.py +++ b/test/models_test.py @@ -681,7 +681,9 @@ def test_repr(self, bg_resolvable): @pytest.fixture def subscriber1(): - return Subscriber(garden="g", namespace="n", system="s", version="v", instance="i", command="c") + return Subscriber( + garden="g", namespace="n", system="s", version="v", instance="i", command="c" + ) @pytest.fixture @@ -702,7 +704,7 @@ def test_repr(self, subscriber1): assert "c" in repr(subscriber1) -class TestTopicSubscribers(): +class TestTopicSubscribers: def test_str(self, topic_subscribers1, subscriber1): print(str(topic_subscribers1)) assert str(topic_subscribers1) == "foo.*: ['g.n.s.v.i.c']" From 0c28f37431131a5b83416c3d0be2292bcabbfb1e Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:55:59 +0000 Subject: [PATCH 05/19] topic and subscriber models --- brewtils/models.py | 43 ++++++++++----------- brewtils/schema_parser.py | 74 ++++++++++++++++++++++++++++++++++++- brewtils/schemas.py | 8 ++-- brewtils/test/comparable.py | 20 ++++++++++ brewtils/test/fixtures.py | 24 +++++++++--- test/models_test.py | 21 +++++++---- test/schema_parser_test.py | 70 +++++++++++++++++++++++++++++++++++ 7 files changed, 218 insertions(+), 42 deletions(-) diff --git a/brewtils/models.py b/brewtils/models.py index 0afed28a..2ba69de2 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -37,8 +37,8 @@ "Garden", "Operation", "Resolvable", - "TopicSubscribers", "Subscriber", + "Topic", ] @@ -1653,24 +1653,7 @@ def __repr__(self): ) -class TopicSubscribers: - schema = "TopicSubscribersSchema" - - def __init__(self, topic=None, subscribers=[]): - self.topic = topic - self.subscribers = subscribers - - def __str__(self): - return "%s: %s" % (self.topic, [str(s) for s in self.subscribers]) - - def __repr__(self): - return "" % ( - self.topic, - self.subscribers, - ) - - -class Subscriber: +class Subscriber(BaseModel): schema = "SubscriberSchema" def __init__( @@ -1690,10 +1673,7 @@ def __init__( self.command = command def __str__(self): - return ( - f"{self.garden}.{self.namespace}.{self.system}.{self.version}.{self.instance}." - f"{self.command}" - ) + return "%s" % self.__dict__ def __repr__(self): return ( @@ -1708,3 +1688,20 @@ def __repr__(self): self.command, ) ) + + +class Topic(BaseModel): + schema = "TopicSchema" + + def __init__(self, name=None, subscribers=None): + self.name = name + self.subscribers = subscribers or [] + + def __str__(self): + return "%s: %s" % (self.name, [str(s) for s in self.subscribers]) + + def __repr__(self): + return "" % ( + self.name, + self.subscribers, + ) diff --git a/brewtils/schema_parser.py b/brewtils/schema_parser.py index 766d5172..cdbc21f4 100644 --- a/brewtils/schema_parser.py +++ b/brewtils/schema_parser.py @@ -51,7 +51,7 @@ class SchemaParser(object): "RunnerSchema": brewtils.models.Runner, "ResolvableSchema": brewtils.models.Resolvable, "SubscriberSchema": brewtils.models.Subscriber, - "TopicSubscribersSchema": brewtils.models.TopicSubscribers, + "TopicSchema": brewtils.models.Topic, } logger = logging.getLogger(__name__) @@ -405,6 +405,38 @@ def parse_resolvable(cls, resolvable, from_string=False, **kwargs): resolvable, brewtils.models.Resolvable, from_string=from_string, **kwargs ) + @classmethod + def parse_subscriber(cls, subscriber, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a subscriber model object + + Args: + subscriber: The raw input + from_string: True if input is a JSON string, False if a dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + A Subscriber object + """ + return cls.parse( + subscriber, brewtils.models.Subscriber, from_string=from_string, **kwargs + ) + + @classmethod + def parse_topic(cls, topic, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a subscriber model object + + Args: + topic: The raw input + from_string: True if input is a JSON string, False if a dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + A Topic object + """ + return cls.parse( + topic, brewtils.models.Topic, from_string=from_string, **kwargs + ) + @classmethod def parse( cls, @@ -871,6 +903,46 @@ def serialize_resolvable(cls, resolvable, to_string=True, **kwargs): **kwargs ) + @classmethod + def serialize_subscriber(cls, subscriber, to_string=True, **kwargs): + """Convert a subscriber model into serialized form + + Args: + subscriber: The subscriber object(s) to be serialized + to_string: True to generate a JSON-formatted string, False to generate a + dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + Serialized representation of subscriber + """ + return cls.serialize( + subscriber, + to_string=to_string, + schema_name=brewtils.models.Subscriber.schema, + **kwargs + ) + + @classmethod + def serialize_topic(cls, topic, to_string=True, **kwargs): + """Convert a topic model into serialized form + + Args: + topic: The topic object(s) to be serialized + to_string: True to generate a JSON-formatted string, False to generate a + dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + Serialized representation of topic + """ + return cls.serialize( + topic, + to_string=to_string, + schema_name=brewtils.models.Topic.schema, + **kwargs + ) + @classmethod def serialize( cls, diff --git a/brewtils/schemas.py b/brewtils/schemas.py index e4c9b04f..7650aa5b 100644 --- a/brewtils/schemas.py +++ b/brewtils/schemas.py @@ -46,8 +46,8 @@ "RoleAssignmentDomainSchema", "GardenDomainIdentifierSchema", "SystemDomainIdentifierSchema", - "TopicSubscribersSchema", "SubscriberSchema", + "TopicSchema", ] # This will be updated after all the schema classes are defined @@ -635,8 +635,8 @@ class SubscriberSchema(BaseSchema): command = fields.Str(allow_none=True) -class TopicSubscribersSchema(BaseSchema): - topic = fields.Str(allow_none=True) +class TopicSchema(BaseSchema): + name = fields.Str(allow_none=True) subscribers = fields.List(fields.Nested(SubscriberSchema, allow_none=True)) @@ -688,7 +688,7 @@ class UserListSchema(BaseSchema): "Runner": RunnerSchema, "Resolvable": ResolvableSchema, "Subscriber": SubscriberSchema, - "TopicSubscribers": TopicSubscribersSchema, + "Topic": TopicSchema, # Compatibility for the Job trigger types "interval": IntervalTriggerSchema, "date": DateTriggerSchema, diff --git a/brewtils/test/comparable.py b/brewtils/test/comparable.py index 2003a7c8..9bade55a 100644 --- a/brewtils/test/comparable.py +++ b/brewtils/test/comparable.py @@ -37,6 +37,8 @@ Resolvable, Runner, System, + Subscriber, + Topic, ) __all__ = [ @@ -58,6 +60,8 @@ "assert_request_file_equal", "assert_operation_equal", "assert_runner_equal", + "assert_subscriber_equal", + "assert_topic_equal", ] @@ -193,6 +197,7 @@ def _assert_wrapper(obj1, obj2, expected_type=None, do_raise=False, **kwargs): assert_runner_equal = partial(_assert_wrapper, expected_type=Runner) assert_resolvable_equal = partial(_assert_wrapper, expected_type=Resolvable) assert_connection_equal = partial(_assert_wrapper, expected_type=Connection) +assert_subscriber_equal = partial(_assert_wrapper, expected_type=Subscriber) def assert_command_equal(obj1, obj2, do_raise=False): @@ -388,3 +393,18 @@ def assert_garden_equal(obj1, obj2, do_raise=False): }, do_raise=do_raise, ) + + +def assert_topic_equal(obj1, obj2, do_raise=False): + print(f"OBJ1: {obj1}") + print(f"OBJ2: {obj2}") + + return _assert_wrapper( + obj1, + obj2, + expected_type=Topic, + deep_fields={ + "subscribers": partial(assert_subscriber_equal, do_raise=True), + }, + do_raise=do_raise, + ) diff --git a/brewtils/test/fixtures.py b/brewtils/test/fixtures.py index f8ad422c..2a429fea 100644 --- a/brewtils/test/fixtures.py +++ b/brewtils/test/fixtures.py @@ -31,7 +31,7 @@ Runner, System, Subscriber, - TopicSubscribers, + Topic, ) @@ -915,12 +915,24 @@ def subscriber_dict(): "namespace": "ns", "system": "system", "version": "1.0.0", - "instance": None, - "command": None, + "instance": "inst", + "command": "run", } @pytest.fixture -def topic_subscriber_dict(subscriber_dict): - """Topic subscribers as dict""" - return {"topic": "foo", "subscribers": [subscriber_dict]} +def bg_subscriber(subscriber_dict): + return Subscriber(**subscriber_dict) + + +@pytest.fixture +def topic_dict(subscriber_dict): + """Topic as dict""" + return {"name": "foo", "subscribers": [subscriber_dict]} + + +@pytest.fixture +def bg_topic(topic_dict, bg_subscriber): + dict_copy = copy.deepcopy(topic_dict) + dict_copy["subscribers"] = [bg_subscriber] + return Topic(**dict_copy) diff --git a/test/models_test.py b/test/models_test.py index 3dbb2dfc..ec3f40dc 100644 --- a/test/models_test.py +++ b/test/models_test.py @@ -20,7 +20,7 @@ RequestTemplate, LegacyRole, Subscriber, - TopicSubscribers, + Topic, ) from pytest_lazyfixture import lazy_fixture @@ -687,13 +687,13 @@ def subscriber1(): @pytest.fixture -def topic_subscribers1(subscriber1): - return TopicSubscribers(topic="foo.*", subscribers=[subscriber1]) +def topic1(subscriber1): + return Topic(name="foo.*", subscribers=[subscriber1]) class TestSubscriber(object): def test_str(self, subscriber1): - assert "g.n.s.v.i.c" == str(subscriber1) + assert str(subscriber1) == "%s" % subscriber1.__dict__ def test_repr(self, subscriber1): assert "g" in repr(subscriber1) @@ -704,7 +704,12 @@ def test_repr(self, subscriber1): assert "c" in repr(subscriber1) -class TestTopicSubscribers: - def test_str(self, topic_subscribers1, subscriber1): - print(str(topic_subscribers1)) - assert str(topic_subscribers1) == "foo.*: ['g.n.s.v.i.c']" +class TestTopic: + def test_str(self, topic1, subscriber1): + assert str(topic1) == "%s: %s" % (topic1.name, [str(subscriber1)]) + + def test_repr(self, topic1, subscriber1): + assert repr(topic1) == "" % ( + topic1.name, + [subscriber1], + ) diff --git a/test/schema_parser_test.py b/test/schema_parser_test.py index cbeee083..9246dba2 100644 --- a/test/schema_parser_test.py +++ b/test/schema_parser_test.py @@ -28,7 +28,9 @@ assert_resolvable_equal, assert_role_equal, assert_runner_equal, + assert_subscriber_equal, assert_system_equal, + assert_topic_equal, ) from marshmallow.exceptions import MarshmallowError from pytest_lazyfixture import lazy_fixture @@ -180,6 +182,18 @@ def test_no_modify(self, system_dict): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + brewtils.models.Subscriber, + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + brewtils.models.Topic, + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_single(self, model, data, assertion, expected): @@ -314,6 +328,18 @@ def test_single_from_string(self): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + "parse_subscriber", + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + "parse_topic", + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_single_specific(self, method, data, assertion, expected): @@ -441,6 +467,18 @@ def test_single_specific_from_string(self): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + brewtils.models.Subscriber, + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + brewtils.models.Topic, + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_many(self, model, data, assertion, expected): @@ -561,6 +599,18 @@ def test_many(self, model, data, assertion, expected): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + "parse_subscriber", + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + "parse_topic", + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_many_specific(self, method, data, assertion, expected): @@ -614,6 +664,8 @@ class TestSerialize(object): (lazy_fixture("bg_operation"), lazy_fixture("operation_dict")), (lazy_fixture("bg_runner"), lazy_fixture("runner_dict")), (lazy_fixture("bg_resolvable"), lazy_fixture("resolvable_dict")), + (lazy_fixture("bg_subscriber"), lazy_fixture("subscriber_dict")), + (lazy_fixture("bg_topic"), lazy_fixture("topic_dict")), ], ) def test_single(self, model, expected): @@ -715,6 +767,16 @@ def test_single(self, model, expected): lazy_fixture("bg_resolvable"), lazy_fixture("resolvable_dict"), ), + ( + "serialize_subscriber", + lazy_fixture("bg_subscriber"), + lazy_fixture("subscriber_dict"), + ), + ( + "serialize_topic", + lazy_fixture("bg_topic"), + lazy_fixture("topic_dict"), + ), ], ) def test_single_specific(self, method, data, expected): @@ -743,6 +805,8 @@ def test_single_specific(self, method, data, expected): (lazy_fixture("bg_operation"), lazy_fixture("operation_dict")), (lazy_fixture("bg_runner"), lazy_fixture("runner_dict")), (lazy_fixture("bg_resolvable"), lazy_fixture("resolvable_dict")), + (lazy_fixture("bg_subscriber"), lazy_fixture("subscriber_dict")), + (lazy_fixture("bg_topic"), lazy_fixture("topic_dict")), ], ) def test_many(self, model, expected): @@ -831,6 +895,12 @@ class TestRoundTrip(object): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + brewtils.models.Subscriber, + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + (brewtils.models.Topic, assert_topic_equal, lazy_fixture("bg_topic")), ], ) def test_parsed_start(self, model, assertion, data): From 1291f115d786154687a9366e220a3097ea5423c9 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:58:22 +0000 Subject: [PATCH 06/19] Support Subscriber object instance comparison --- brewtils/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/brewtils/models.py b/brewtils/models.py index 2ba69de2..4e5d8eec 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -1689,6 +1689,20 @@ def __repr__(self): ) ) + def __eq__(self, other): + if not isinstance(other, Subscriber): + # don't attempt to compare against unrelated types + return NotImplemented + + return ( + self.garden == other.garden + and self.namespace == other.namespace + and self.system == other.system + and self.version == other.version + and self.instance == other.instance + and self.command == other.command + ) + class Topic(BaseModel): schema = "TopicSchema" From 99fc978d57cc389ef14bbbc42d47491f403d1a80 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:51:58 +0000 Subject: [PATCH 07/19] Add Topic to easy client --- brewtils/rest/client.py | 72 +++++++++++++++++++++++++++++++++++ brewtils/rest/easy_client.py | 55 ++++++++++++++++++++++++++ test/rest/client_test.py | 29 ++++++++++++++ test/rest/easy_client_test.py | 45 ++++++++++++++++++++++ 4 files changed, 201 insertions(+) diff --git a/brewtils/rest/client.py b/brewtils/rest/client.py index 81b415f1..728fdbbb 100644 --- a/brewtils/rest/client.py +++ b/brewtils/rest/client.py @@ -153,6 +153,7 @@ def __init__(self, *args, **kwargs): self.user_url = self.base_url + "api/v1/users/" self.admin_url = self.base_url + "api/v1/admin/" self.forward_url = self.base_url + "api/v1/forward" + self.topic_url = self.base_url + "api/v1/topic/" # Deprecated self.logging_config_url = self.base_url + "api/v1/config/logging/" @@ -943,3 +944,74 @@ def get_tokens(self, username=None, password=None): self.session.headers["Authorization"] = "Bearer " + self.access_token return response + + @enable_auth + def get_topic(self, topic_name, **kwargs): + # type: (str, **Any) -> Response + """Performs a GET on the Topic URL + + Args: + topic_name: Topic Name + **kwargs: Query parameters to be used in the GET request + + Returns: + Requests Response object + """ + return self.session.get(self.topic_url + quote(topic_name), params=kwargs) + + @enable_auth + def get_topics(self, **kwargs): + # type: (**Any) -> Response + """Perform a GET on the Topic URL + + Args: + **kwargs: Query parameters to be used in the GET request + + Returns: + Requests Response object + """ + return self.session.get(self.topic_url, params=kwargs) + + @enable_auth + def post_topics(self, payload): + # type: (str) -> Response + """Performs a POST on the Topic URL + + Args: + payload: New Topic definition + + Returns: + Requests Response object + """ + return self.session.post( + self.topic_url, data=payload, headers=self.JSON_HEADERS + ) + + @enable_auth + def patch_topic(self, topic_name, payload): + # type: (str, str) -> Response + """Performs a PATCH on a Topic URL + + Args: + topic_name: Topic name + payload: Serialized PatchOperation + + Returns: + Requests Response object + """ + return self.session.patch( + self.topic_url + str(topic_name), data=payload, headers=self.JSON_HEADERS + ) + + @enable_auth + def delete_topic(self, topic_name): + # type: (str) -> Response + """Performs a DELETE on a Topic URL + + Args: + topic_name: Topic name + + Returns: + Requests Response object + """ + return self.session.delete(self.topic_url + quote(topic_name)) diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index 575d728a..66163f1b 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1156,3 +1156,58 @@ def _check_chunked_file_validity(self, file_id): return True, metadata_json else: return False, metadata_json + + @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=FetchError) + def get_topic(self, topic_name): + """Get a topic + + Args: + topic_name: Topic name + + Returns: + The Topic + + """ + return self.client.get_topic(topic_name) + + @wrap_response(parse_method="parse_topic", parse_many=True, default_exc=FetchError) + def find_topics(self, **kwargs): + """Find Topics using keyword arguments as search parameters + + Args: + **kwargs: Search parameters + + Returns: + List[Topics]: List of Topics matching the search parameters + + """ + return self.client.get_topics(**kwargs) + + @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=SaveError) + def create_topic(self, topic): + """Create a new Topic + + Args: + system (Topic): The Topic to create + + Returns: + Topic: The newly-created topic + + """ + return self.client.post_topics(SchemaParser.serialize_topic(topic)) + + @wrap_response(return_boolean=True, raise_404=True) + def remove_topic(self, topic_name): + """Remove a unique Topic + + Args: + topic_name: Topic name + + Returns: + bool: True if removal was successful + + Raises: + NotFoundError: Couldn't find a Topic matching given parameters + + """ + return self.client.delete_topic(topic_name) diff --git a/test/rest/client_test.py b/test/rest/client_test.py index 51f560b5..86eee654 100644 --- a/test/rest/client_test.py +++ b/test/rest/client_test.py @@ -112,6 +112,7 @@ def test_non_versioned_uris(self, client, url_prefix): ("logging_config_url", "http://host:80%sapi/v1/config/logging/"), ("job_url", "http://host:80%sapi/v1/jobs/"), ("token_url", "http://host:80%sapi/v1/token/"), + ("topic_url", "http://host:80%sapi/v1/topic/"), ("user_url", "http://host:80%sapi/v1/users/"), ("admin_url", "http://host:80%sapi/v1/admin/"), ], @@ -131,6 +132,7 @@ def test_version_1_uri(self, url_prefix, client, url, expected): ("logging_config_url", "https://host:80%sapi/v1/config/logging/"), ("job_url", "https://host:80%sapi/v1/jobs/"), ("token_url", "https://host:80%sapi/v1/token/"), + ("topic_url", "https://host:80%sapi/v1/topic/"), ("user_url", "https://host:80%sapi/v1/users/"), ("admin_url", "https://host:80%sapi/v1/admin/"), ], @@ -150,6 +152,7 @@ def test_version_1_uri_ssl(self, url_prefix, ssl_client, url, expected): "logging_url", ), ("get_systems", {"key": "value"}, "get", "system_url"), + ("get_topics", {"key": "value"}, "get", "topic_url"), ], ) def test_version_1_gets(self, client, session_mock, method, params, verb, url): @@ -470,3 +473,29 @@ def test_client_cert_without_username_password(self, monkeypatch): client.get_garden("somegarden") assert get_tokens_mock.called is True + + def test_get_topic(self, client, session_mock): + client.get_topic("name!") + session_mock.get.assert_called_with(client.topic_url + "name%21", params={}) + + def test_get_topics(self, client, session_mock): + client.get_topics() + session_mock.get_assert_called_with(client.topic_url, params={}) + + def test_post_topics(self, client, session_mock): + client.post_topics(payload="payload") + session_mock.post.assert_called_with( + client.topic_url, data="payload", headers=client.JSON_HEADERS + ) + + def test_patch_topic(self, client, session_mock): + client.patch_topic("topicname", "payload") + session_mock.patch.assert_called_with( + client.topic_url + "topicname", + data="payload", + headers=client.JSON_HEADERS, + ) + + def test_delete_topic(self, client, session_mock): + client.delete_topic("name!") + session_mock.delete.assert_called_with(client.topic_url + "name%21") diff --git a/test/rest/easy_client_test.py b/test/rest/easy_client_test.py index 488bb27c..19465b7a 100644 --- a/test/rest/easy_client_test.py +++ b/test/rest/easy_client_test.py @@ -602,3 +602,48 @@ def test_upload_file_fail(self, client, rest_client, server_error, target_file): rest_client.post_chunked_file.return_value = server_error with pytest.raises(SaveError): assert client.upload_chunked_file(target_file, "desired_name") + + +class TestTopics(object): + class TestGet(object): + def test_success(self, client, rest_client, bg_topic, success, parser): + rest_client.get_topic.return_value = success + parser.parse_topic.return_value = bg_topic + + assert client.get_topic(bg_topic.name) == bg_topic + + def test_404(self, client, rest_client, bg_topic, not_found): + rest_client.get_topic.return_value = not_found + + with pytest.raises(NotFoundError): + client.get_topic(bg_topic.name) + + class TestFind(object): + def test_success(self, client, rest_client, success): + rest_client.get_topics.return_value = success + client.find_topics() + assert rest_client.get_topics.called is True + + def test_with_params(self, client, rest_client, success): + rest_client.get_topics.return_value = success + client.find_topics(name="foo") + rest_client.get_topics.assert_called_once_with(name="foo") + + def test_create(self, client, rest_client, success, bg_topic): + rest_client.create_topic.return_value = success + client.create_topic(bg_topic) + assert rest_client.post_topics.called is True + + class TestRemove(object): + def test_not_found(self, monkeypatch, client, rest_client, not_found, bg_topic): + monkeypatch.setattr( + rest_client, "delete_topic", Mock(return_value=not_found) + ) + + with pytest.raises(NotFoundError): + client.remove_topic(bg_topic.name) + + def test_name(self, client, rest_client, success, bg_topic): + rest_client.delete_topic.return_value = success + + assert client.remove_topic(bg_topic.name) From c0c19d7451652bbf4b6175a3f528341476f4d776 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:29:09 +0000 Subject: [PATCH 08/19] Add Topic Id --- brewtils/models.py | 3 ++- brewtils/schemas.py | 1 + brewtils/test/comparable.py | 3 --- brewtils/test/fixtures.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/brewtils/models.py b/brewtils/models.py index 4e5d8eec..b70d22ed 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -1707,7 +1707,8 @@ def __eq__(self, other): class Topic(BaseModel): schema = "TopicSchema" - def __init__(self, name=None, subscribers=None): + def __init__(self, id=None, name=None, subscribers=None): # noqa # shadows built-in + self.id = id self.name = name self.subscribers = subscribers or [] diff --git a/brewtils/schemas.py b/brewtils/schemas.py index 7650aa5b..6bfe08c1 100644 --- a/brewtils/schemas.py +++ b/brewtils/schemas.py @@ -636,6 +636,7 @@ class SubscriberSchema(BaseSchema): class TopicSchema(BaseSchema): + id = fields.Str(allow_none=True) name = fields.Str(allow_none=True) subscribers = fields.List(fields.Nested(SubscriberSchema, allow_none=True)) diff --git a/brewtils/test/comparable.py b/brewtils/test/comparable.py index 9bade55a..1dda4a73 100644 --- a/brewtils/test/comparable.py +++ b/brewtils/test/comparable.py @@ -396,9 +396,6 @@ def assert_garden_equal(obj1, obj2, do_raise=False): def assert_topic_equal(obj1, obj2, do_raise=False): - print(f"OBJ1: {obj1}") - print(f"OBJ2: {obj2}") - return _assert_wrapper( obj1, obj2, diff --git a/brewtils/test/fixtures.py b/brewtils/test/fixtures.py index 2a429fea..6cb362fc 100644 --- a/brewtils/test/fixtures.py +++ b/brewtils/test/fixtures.py @@ -928,7 +928,7 @@ def bg_subscriber(subscriber_dict): @pytest.fixture def topic_dict(subscriber_dict): """Topic as dict""" - return {"name": "foo", "subscribers": [subscriber_dict]} + return {"id": "5d174df1", "name": "foo", "subscribers": [subscriber_dict]} @pytest.fixture From 43b1f6fcbcb6a63c52a2da3e298f16b2dab58d3d Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:56:57 +0000 Subject: [PATCH 09/19] Add topic events --- brewtils/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/brewtils/models.py b/brewtils/models.py index b70d22ed..7bfbcbef 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -96,8 +96,11 @@ class Events(Enum): COMMAND_PUBLISHING_BLOCKLIST_SYNC = 48 COMMAND_PUBLISHING_BLOCKLIST_REMOVE = 49 COMMAND_PUBLISHING_BLOCKLIST_UPDATE = 50 + TOPIC_CREATED = 54 + TOPIC_UPDATED = 55 + TOPIC_REMOVED = 56 - # Next: 54 + # Next: 57 class BaseModel(object): From caa6a8663cccdd7b9dc1222b85c2ac4065799b88 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:09:09 +0000 Subject: [PATCH 10/19] Update client with topic id --- brewtils/rest/client.py | 20 ++++++++++---------- brewtils/rest/easy_client.py | 12 ++++++------ test/rest/client_test.py | 16 ++++++++-------- test/rest/easy_client_test.py | 10 +++++----- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/brewtils/rest/client.py b/brewtils/rest/client.py index 728fdbbb..87ddabf4 100644 --- a/brewtils/rest/client.py +++ b/brewtils/rest/client.py @@ -153,7 +153,7 @@ def __init__(self, *args, **kwargs): self.user_url = self.base_url + "api/v1/users/" self.admin_url = self.base_url + "api/v1/admin/" self.forward_url = self.base_url + "api/v1/forward" - self.topic_url = self.base_url + "api/v1/topic/" + self.topic_url = self.base_url + "api/v1/topics/" # Deprecated self.logging_config_url = self.base_url + "api/v1/config/logging/" @@ -946,18 +946,18 @@ def get_tokens(self, username=None, password=None): return response @enable_auth - def get_topic(self, topic_name, **kwargs): + def get_topic(self, topic_id, **kwargs): # type: (str, **Any) -> Response """Performs a GET on the Topic URL Args: - topic_name: Topic Name + topic_id: Topic id **kwargs: Query parameters to be used in the GET request Returns: Requests Response object """ - return self.session.get(self.topic_url + quote(topic_name), params=kwargs) + return self.session.get(self.topic_url + topic_id, params=kwargs) @enable_auth def get_topics(self, **kwargs): @@ -988,30 +988,30 @@ def post_topics(self, payload): ) @enable_auth - def patch_topic(self, topic_name, payload): + def patch_topic(self, topic_id, payload): # type: (str, str) -> Response """Performs a PATCH on a Topic URL Args: - topic_name: Topic name + topic_id: Topic id payload: Serialized PatchOperation Returns: Requests Response object """ return self.session.patch( - self.topic_url + str(topic_name), data=payload, headers=self.JSON_HEADERS + self.topic_url + topic_id, data=payload, headers=self.JSON_HEADERS ) @enable_auth - def delete_topic(self, topic_name): + def delete_topic(self, topic_id): # type: (str) -> Response """Performs a DELETE on a Topic URL Args: - topic_name: Topic name + topic_id: Topic id Returns: Requests Response object """ - return self.session.delete(self.topic_url + quote(topic_name)) + return self.session.delete(self.topic_url + topic_id) diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index 66163f1b..12a92907 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1158,17 +1158,17 @@ def _check_chunked_file_validity(self, file_id): return False, metadata_json @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=FetchError) - def get_topic(self, topic_name): + def get_topic(self, topic_id): """Get a topic Args: - topic_name: Topic name + topic_id: Topic id Returns: The Topic """ - return self.client.get_topic(topic_name) + return self.client.get_topic(topic_id) @wrap_response(parse_method="parse_topic", parse_many=True, default_exc=FetchError) def find_topics(self, **kwargs): @@ -1197,11 +1197,11 @@ def create_topic(self, topic): return self.client.post_topics(SchemaParser.serialize_topic(topic)) @wrap_response(return_boolean=True, raise_404=True) - def remove_topic(self, topic_name): + def remove_topic(self, topic_id): """Remove a unique Topic Args: - topic_name: Topic name + topic_id: Topic id Returns: bool: True if removal was successful @@ -1210,4 +1210,4 @@ def remove_topic(self, topic_name): NotFoundError: Couldn't find a Topic matching given parameters """ - return self.client.delete_topic(topic_name) + return self.client.delete_topic(topic_id) diff --git a/test/rest/client_test.py b/test/rest/client_test.py index 86eee654..76e4cf10 100644 --- a/test/rest/client_test.py +++ b/test/rest/client_test.py @@ -112,7 +112,7 @@ def test_non_versioned_uris(self, client, url_prefix): ("logging_config_url", "http://host:80%sapi/v1/config/logging/"), ("job_url", "http://host:80%sapi/v1/jobs/"), ("token_url", "http://host:80%sapi/v1/token/"), - ("topic_url", "http://host:80%sapi/v1/topic/"), + ("topic_url", "http://host:80%sapi/v1/topics/"), ("user_url", "http://host:80%sapi/v1/users/"), ("admin_url", "http://host:80%sapi/v1/admin/"), ], @@ -132,7 +132,7 @@ def test_version_1_uri(self, url_prefix, client, url, expected): ("logging_config_url", "https://host:80%sapi/v1/config/logging/"), ("job_url", "https://host:80%sapi/v1/jobs/"), ("token_url", "https://host:80%sapi/v1/token/"), - ("topic_url", "https://host:80%sapi/v1/topic/"), + ("topic_url", "https://host:80%sapi/v1/topics/"), ("user_url", "https://host:80%sapi/v1/users/"), ("admin_url", "https://host:80%sapi/v1/admin/"), ], @@ -475,8 +475,8 @@ def test_client_cert_without_username_password(self, monkeypatch): assert get_tokens_mock.called is True def test_get_topic(self, client, session_mock): - client.get_topic("name!") - session_mock.get.assert_called_with(client.topic_url + "name%21", params={}) + client.get_topic("id") + session_mock.get.assert_called_with(client.topic_url + "id", params={}) def test_get_topics(self, client, session_mock): client.get_topics() @@ -489,13 +489,13 @@ def test_post_topics(self, client, session_mock): ) def test_patch_topic(self, client, session_mock): - client.patch_topic("topicname", "payload") + client.patch_topic("id", "payload") session_mock.patch.assert_called_with( - client.topic_url + "topicname", + client.topic_url + "id", data="payload", headers=client.JSON_HEADERS, ) def test_delete_topic(self, client, session_mock): - client.delete_topic("name!") - session_mock.delete.assert_called_with(client.topic_url + "name%21") + client.delete_topic("id") + session_mock.delete.assert_called_with(client.topic_url + "id") diff --git a/test/rest/easy_client_test.py b/test/rest/easy_client_test.py index 19465b7a..8d686eef 100644 --- a/test/rest/easy_client_test.py +++ b/test/rest/easy_client_test.py @@ -610,13 +610,13 @@ def test_success(self, client, rest_client, bg_topic, success, parser): rest_client.get_topic.return_value = success parser.parse_topic.return_value = bg_topic - assert client.get_topic(bg_topic.name) == bg_topic + assert client.get_topic(bg_topic.id) == bg_topic def test_404(self, client, rest_client, bg_topic, not_found): rest_client.get_topic.return_value = not_found with pytest.raises(NotFoundError): - client.get_topic(bg_topic.name) + client.get_topic(bg_topic.id) class TestFind(object): def test_success(self, client, rest_client, success): @@ -641,9 +641,9 @@ def test_not_found(self, monkeypatch, client, rest_client, not_found, bg_topic): ) with pytest.raises(NotFoundError): - client.remove_topic(bg_topic.name) + client.remove_topic(bg_topic.id) - def test_name(self, client, rest_client, success, bg_topic): + def test_id(self, client, rest_client, success, bg_topic): rest_client.delete_topic.return_value = success - assert client.remove_topic(bg_topic.name) + assert client.remove_topic(bg_topic.id) From ffc3d896c89aa493bdc151644a270b9520dd5b7b Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:30:53 +0000 Subject: [PATCH 11/19] Rework get topics --- brewtils/rest/client.py | 9 +++------ brewtils/rest/easy_client.py | 11 ++++------- test/rest/client_test.py | 1 - test/rest/easy_client_test.py | 20 +++++++++----------- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/brewtils/rest/client.py b/brewtils/rest/client.py index 87ddabf4..0a5165d2 100644 --- a/brewtils/rest/client.py +++ b/brewtils/rest/client.py @@ -960,17 +960,14 @@ def get_topic(self, topic_id, **kwargs): return self.session.get(self.topic_url + topic_id, params=kwargs) @enable_auth - def get_topics(self, **kwargs): - # type: (**Any) -> Response + def get_topics(self): + # type: () -> Response """Perform a GET on the Topic URL - Args: - **kwargs: Query parameters to be used in the GET request - Returns: Requests Response object """ - return self.session.get(self.topic_url, params=kwargs) + return self.session.get(self.topic_url) @enable_auth def post_topics(self, payload): diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index 12a92907..3f54a203 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1171,17 +1171,14 @@ def get_topic(self, topic_id): return self.client.get_topic(topic_id) @wrap_response(parse_method="parse_topic", parse_many=True, default_exc=FetchError) - def find_topics(self, **kwargs): - """Find Topics using keyword arguments as search parameters - - Args: - **kwargs: Search parameters + def get_topics(self): + """Get all Topics Returns: - List[Topics]: List of Topics matching the search parameters + List[Topics]: List of Topics """ - return self.client.get_topics(**kwargs) + return self.client.get_topics() @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=SaveError) def create_topic(self, topic): diff --git a/test/rest/client_test.py b/test/rest/client_test.py index 76e4cf10..84e43d82 100644 --- a/test/rest/client_test.py +++ b/test/rest/client_test.py @@ -152,7 +152,6 @@ def test_version_1_uri_ssl(self, url_prefix, ssl_client, url, expected): "logging_url", ), ("get_systems", {"key": "value"}, "get", "system_url"), - ("get_topics", {"key": "value"}, "get", "topic_url"), ], ) def test_version_1_gets(self, client, session_mock, method, params, verb, url): diff --git a/test/rest/easy_client_test.py b/test/rest/easy_client_test.py index 8d686eef..503a091c 100644 --- a/test/rest/easy_client_test.py +++ b/test/rest/easy_client_test.py @@ -618,22 +618,20 @@ def test_404(self, client, rest_client, bg_topic, not_found): with pytest.raises(NotFoundError): client.get_topic(bg_topic.id) - class TestFind(object): - def test_success(self, client, rest_client, success): - rest_client.get_topics.return_value = success - client.find_topics() - assert rest_client.get_topics.called is True - - def test_with_params(self, client, rest_client, success): - rest_client.get_topics.return_value = success - client.find_topics(name="foo") - rest_client.get_topics.assert_called_once_with(name="foo") - def test_create(self, client, rest_client, success, bg_topic): rest_client.create_topic.return_value = success client.create_topic(bg_topic) assert rest_client.post_topics.called is True + def test_get_all(self, client, rest_client, bg_topic, success, parser): + second_topic = copy.deepcopy(bg_topic) + second_topic.name = "topic2" + both_topics = [bg_topic, second_topic] + rest_client.get_topics.return_value = success + parser.parse_topic.return_value = both_topics + + assert client.get_topics() == both_topics + class TestRemove(object): def test_not_found(self, monkeypatch, client, rest_client, not_found, bg_topic): monkeypatch.setattr( From 8c9fc389431942c05b8cafc4c8fbd30a1059cefa Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:58:31 -0400 Subject: [PATCH 12/19] Update __version__.py --- brewtils/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brewtils/__version__.py b/brewtils/__version__.py index 42dbaac3..36b6b572 100644 --- a/brewtils/__version__.py +++ b/brewtils/__version__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "3.24.2" +__version__ = "3.24.3" From b681da1111694188330d6aeae040546589d8bb23 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Tue, 12 Mar 2024 05:47:07 -0400 Subject: [PATCH 13/19] Update __version__.py --- brewtils/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brewtils/__version__.py b/brewtils/__version__.py index 36b6b572..467dab72 100644 --- a/brewtils/__version__.py +++ b/brewtils/__version__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "3.24.3" +__version__ = "3.24.4" From 3457dd5afef15c1f4fdb59855679f0b648e2bf18 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:58:02 +0000 Subject: [PATCH 14/19] add update topic to easy client --- brewtils/rest/easy_client.py | 26 ++++++++++++++++++++++++++ test/rest/easy_client_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index 3f54a203..40f1897d 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1208,3 +1208,29 @@ def remove_topic(self, topic_id): """ return self.client.delete_topic(topic_id) + + @wrap_response( + parse_method="parse_topic", parse_many=False, default_exc=SaveError + ) + def update_topic(self, topic_id, add=None, remove=None): + """Update a Topic + + Args: + topic_id (str): The Topic ID + add (Optional[str]): Add subscriber + remove (Optional[str]): Remove subscriber + + Returns: + Topic: The updated topic + + """ + operations = [] + + if add: + operations.append(PatchOperation("add_subscriber", add)) + if remove: + operations.append(PatchOperation("remove_subscriber", remove)) + + return self.client.patch_topic( + topic_id, SchemaParser.serialize_patch(operations, many=True) + ) diff --git a/test/rest/easy_client_test.py b/test/rest/easy_client_test.py index 503a091c..b5275742 100644 --- a/test/rest/easy_client_test.py +++ b/test/rest/easy_client_test.py @@ -645,3 +645,29 @@ def test_id(self, client, rest_client, success, bg_topic): rest_client.delete_topic.return_value = success assert client.remove_topic(bg_topic.id) + + class TestPatch(object): + def test_add_subscriber(self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber): + monkeypatch.setattr( + rest_client, "patch_topic", Mock(return_value=success) + ) + + assert client.update_topic(bg_topic.id, add=bg_subscriber) + assert rest_client.patch_topic.called is True + + def test_remove_subscriber(self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber): + monkeypatch.setattr( + rest_client, "patch_topic", Mock(return_value=success) + ) + + assert client.update_topic(bg_topic.id, remove=bg_subscriber) + assert rest_client.patch_topic.called is True + + def test_remove_subscriber_not_found(self, monkeypatch, client, rest_client, not_found, bg_topic, bg_subscriber): + monkeypatch.setattr( + rest_client, "patch_topic", Mock(return_value=not_found) + ) + + with pytest.raises(NotFoundError): + assert client.update_topic(bg_topic.id, remove=bg_subscriber) + assert rest_client.patch_topic.called is True From 026bba3b2635c9d05a4f154c36a516a38c73337b Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:08:27 +0000 Subject: [PATCH 15/19] Changelog and formatting --- CHANGELOG.rst | 6 ++++++ brewtils/rest/easy_client.py | 4 +--- test/rest/easy_client_test.py | 20 +++++++++++--------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aa996d26..a35ddd1f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Brewtils Changelog ================== +3.25.1 +------ +TBD + +- Added Topic and Subscriber models and related access methods to easy client + 3.24.4 ------ 3/11/2024 diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index 40f1897d..ab62f358 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1209,9 +1209,7 @@ def remove_topic(self, topic_id): """ return self.client.delete_topic(topic_id) - @wrap_response( - parse_method="parse_topic", parse_many=False, default_exc=SaveError - ) + @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=SaveError) def update_topic(self, topic_id, add=None, remove=None): """Update a Topic diff --git a/test/rest/easy_client_test.py b/test/rest/easy_client_test.py index b5275742..f51b7c35 100644 --- a/test/rest/easy_client_test.py +++ b/test/rest/easy_client_test.py @@ -647,23 +647,25 @@ def test_id(self, client, rest_client, success, bg_topic): assert client.remove_topic(bg_topic.id) class TestPatch(object): - def test_add_subscriber(self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber): - monkeypatch.setattr( - rest_client, "patch_topic", Mock(return_value=success) - ) + def test_add_subscriber( + self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber + ): + monkeypatch.setattr(rest_client, "patch_topic", Mock(return_value=success)) assert client.update_topic(bg_topic.id, add=bg_subscriber) assert rest_client.patch_topic.called is True - def test_remove_subscriber(self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber): - monkeypatch.setattr( - rest_client, "patch_topic", Mock(return_value=success) - ) + def test_remove_subscriber( + self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber + ): + monkeypatch.setattr(rest_client, "patch_topic", Mock(return_value=success)) assert client.update_topic(bg_topic.id, remove=bg_subscriber) assert rest_client.patch_topic.called is True - def test_remove_subscriber_not_found(self, monkeypatch, client, rest_client, not_found, bg_topic, bg_subscriber): + def test_remove_subscriber_not_found( + self, monkeypatch, client, rest_client, not_found, bg_topic, bg_subscriber + ): monkeypatch.setattr( rest_client, "patch_topic", Mock(return_value=not_found) ) From 7de573ab27786b15d23c759ca8f4da2a9105e385 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:13:48 +0000 Subject: [PATCH 16/19] Remove kwargs from get_topic in client --- brewtils/rest/client.py | 5 ++--- test/rest/client_test.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/brewtils/rest/client.py b/brewtils/rest/client.py index 0a5165d2..e35597db 100644 --- a/brewtils/rest/client.py +++ b/brewtils/rest/client.py @@ -946,18 +946,17 @@ def get_tokens(self, username=None, password=None): return response @enable_auth - def get_topic(self, topic_id, **kwargs): + def get_topic(self, topic_id): # type: (str, **Any) -> Response """Performs a GET on the Topic URL Args: topic_id: Topic id - **kwargs: Query parameters to be used in the GET request Returns: Requests Response object """ - return self.session.get(self.topic_url + topic_id, params=kwargs) + return self.session.get(self.topic_url + topic_id) @enable_auth def get_topics(self): diff --git a/test/rest/client_test.py b/test/rest/client_test.py index 84e43d82..57e267dc 100644 --- a/test/rest/client_test.py +++ b/test/rest/client_test.py @@ -475,7 +475,7 @@ def test_client_cert_without_username_password(self, monkeypatch): def test_get_topic(self, client, session_mock): client.get_topic("id") - session_mock.get.assert_called_with(client.topic_url + "id", params={}) + session_mock.get.assert_called_with(client.topic_url + "id") def test_get_topics(self, client, session_mock): client.get_topics() From f415f4ba547f7e35a6601743dc3da0aaf172dc08 Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:08:20 -0400 Subject: [PATCH 17/19] Update easy_client.py for topic patch --- brewtils/rest/easy_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index ab62f358..d95ef079 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1225,9 +1225,9 @@ def update_topic(self, topic_id, add=None, remove=None): operations = [] if add: - operations.append(PatchOperation("add_subscriber", add)) + operations.append(PatchOperation("add", add)) if remove: - operations.append(PatchOperation("remove_subscriber", remove)) + operations.append(PatchOperation("remove", remove)) return self.client.patch_topic( topic_id, SchemaParser.serialize_patch(operations, many=True) From 60b7369fd247c6aab3ed76191aba015f20941e4e Mon Sep 17 00:00:00 2001 From: 1maple1 <160027655+1maple1@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:56:05 -0400 Subject: [PATCH 18/19] Update easy_client.py to set value on update topic --- brewtils/rest/easy_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index d95ef079..42b2c408 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1225,9 +1225,9 @@ def update_topic(self, topic_id, add=None, remove=None): operations = [] if add: - operations.append(PatchOperation("add", add)) + operations.append(PatchOperation("add", value=add)) if remove: - operations.append(PatchOperation("remove", remove)) + operations.append(PatchOperation("remove", value=remove)) return self.client.patch_topic( topic_id, SchemaParser.serialize_patch(operations, many=True) From cac2a004bdc62671115d5ffb3e395e8d7c0ccc2a Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:37:12 -0400 Subject: [PATCH 19/19] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a35ddd1f..e78cf4a4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Brewtils Changelog ================== -3.25.1 +3.25.0 ------ TBD