From 1a9ef612e53f4559ddb6be4e057bb99cb502b288 Mon Sep 17 00:00:00 2001 From: AlexCK-STFC <210735915+AlexCK-STFC@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:36:57 +0100 Subject: [PATCH 1/2] ENH: Add "PROJECT_TAGS" property This is a JSON-formatted list. It will usually include a contact email for the project --- docs/user_docs/query_docs/PROJECTS.md | 1 + openstackquery/enums/props/project_properties.py | 5 +++++ openstackquery/mappings/project_mapping.py | 2 ++ tests/enums/props/test_project_properties.py | 3 ++- tests/mappings/test_project_mapping.py | 2 ++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/user_docs/query_docs/PROJECTS.md b/docs/user_docs/query_docs/PROJECTS.md index f9396a0..fbdeffc 100644 --- a/docs/user_docs/query_docs/PROJECTS.md +++ b/docs/user_docs/query_docs/PROJECTS.md @@ -27,6 +27,7 @@ A `Project` has the following properties: | `bool` | "is_enabled" | Indicates whether users can authorize against this project.
if set to False, users cannot access project, additionally all authorized tokens are invalidated. | | `string` | "name" | Name of the project. | | `string` | "parent_id" | The ID of the parent of the project. | +| `string` | "tags" | JSON-formatted string of a list of project tags (usually one of these is an email) | Any of these properties can be used for any of the API methods that takes a property - like `select`, `where`, `sort_by` etc diff --git a/openstackquery/enums/props/project_properties.py b/openstackquery/enums/props/project_properties.py index 4b83e05..f34c903 100644 --- a/openstackquery/enums/props/project_properties.py +++ b/openstackquery/enums/props/project_properties.py @@ -1,4 +1,6 @@ +import json from enum import auto + from openstackquery.enums.props.prop_enum import PropEnum from openstackquery.exceptions.query_property_mapping_error import ( QueryPropertyMappingError, @@ -17,6 +19,7 @@ class ProjectProperties(PropEnum): PROJECT_IS_ENABLED = auto() PROJECT_NAME = auto() PROJECT_PARENT_ID = auto() + PROJECT_TAGS = auto() @staticmethod def _get_aliases(): @@ -31,6 +34,7 @@ def _get_aliases(): ProjectProperties.PROJECT_IS_ENABLED: ["is_enabled"], ProjectProperties.PROJECT_NAME: ["name"], ProjectProperties.PROJECT_PARENT_ID: ["parent_id"], + ProjectProperties.PROJECT_TAGS: ["tags"], } @staticmethod @@ -49,6 +53,7 @@ def get_prop_mapping(prop): ProjectProperties.PROJECT_IS_ENABLED: lambda a: a["is_enabled"], ProjectProperties.PROJECT_NAME: lambda a: a["name"], ProjectProperties.PROJECT_PARENT_ID: lambda a: a["parent_id"], + ProjectProperties.PROJECT_TAGS: lambda a: json.dumps(a.get("tags", [])), } try: return mapping[prop] diff --git a/openstackquery/mappings/project_mapping.py b/openstackquery/mappings/project_mapping.py index 4ce582a..b76774e 100644 --- a/openstackquery/mappings/project_mapping.py +++ b/openstackquery/mappings/project_mapping.py @@ -107,10 +107,12 @@ def get_client_side_handler() -> ClientSideHandler: QueryPresets.MATCHES_REGEX: [ ProjectProperties.PROJECT_NAME, ProjectProperties.PROJECT_DESCRIPTION, + ProjectProperties.PROJECT_TAGS, ], QueryPresets.NOT_MATCHES_REGEX: [ ProjectProperties.PROJECT_NAME, ProjectProperties.PROJECT_DESCRIPTION, + ProjectProperties.PROJECT_TAGS, ], } ) diff --git a/tests/enums/props/test_project_properties.py b/tests/enums/props/test_project_properties.py index d9786f8..f783ece 100644 --- a/tests/enums/props/test_project_properties.py +++ b/tests/enums/props/test_project_properties.py @@ -1,11 +1,11 @@ from unittest.mock import patch + import pytest from openstackquery.enums.props.project_properties import ProjectProperties from openstackquery.exceptions.query_property_mapping_error import ( QueryPropertyMappingError, ) - from tests.mocks.mocked_props import MockProperties @@ -25,6 +25,7 @@ ), (ProjectProperties.PROJECT_NAME, ["project_name", "name"]), (ProjectProperties.PROJECT_PARENT_ID, ["project_parent_id", "parent_id"]), + (ProjectProperties.PROJECT_TAGS, ["project_tags", "tags"]), ], ) def test_property_serialization(expected_prop, test_values, property_variant_generator): diff --git a/tests/mappings/test_project_mapping.py b/tests/mappings/test_project_mapping.py index c4c3e6f..67ab11f 100644 --- a/tests/mappings/test_project_mapping.py +++ b/tests/mappings/test_project_mapping.py @@ -115,10 +115,12 @@ def test_client_side_handlers_string(client_side_test_mappings): QueryPresets.MATCHES_REGEX: [ ProjectProperties.PROJECT_NAME, ProjectProperties.PROJECT_DESCRIPTION, + ProjectProperties.PROJECT_TAGS, ], QueryPresets.NOT_MATCHES_REGEX: [ ProjectProperties.PROJECT_NAME, ProjectProperties.PROJECT_DESCRIPTION, + ProjectProperties.PROJECT_TAGS, ], } client_side_test_mappings(handler, mappings) From 8dc3581a1d8494718eda92dcafda1155d60d265b Mon Sep 17 00:00:00 2001 From: AlexCK-STFC <210735915+AlexCK-STFC@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:09:45 +0100 Subject: [PATCH 2/2] ENH: Add PROJECT_EMAIL --- docs/user_docs/query_docs/PROJECTS.md | 1 + .../enums/props/project_properties.py | 17 +++++++++++ openstackquery/mappings/project_mapping.py | 2 ++ tests/enums/props/test_project_properties.py | 29 +++++++++++++++++++ tests/mappings/test_project_mapping.py | 18 +++++------- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/docs/user_docs/query_docs/PROJECTS.md b/docs/user_docs/query_docs/PROJECTS.md index fbdeffc..a64f37a 100644 --- a/docs/user_docs/query_docs/PROJECTS.md +++ b/docs/user_docs/query_docs/PROJECTS.md @@ -28,6 +28,7 @@ A `Project` has the following properties: | `string` | "name" | Name of the project. | | `string` | "parent_id" | The ID of the parent of the project. | | `string` | "tags" | JSON-formatted string of a list of project tags (usually one of these is an email) | +| `string` | "email" | The first email found in tags, or None (which outputs in results as 'Not Found'). | Any of these properties can be used for any of the API methods that takes a property - like `select`, `where`, `sort_by` etc diff --git a/openstackquery/enums/props/project_properties.py b/openstackquery/enums/props/project_properties.py index f34c903..09b0a86 100644 --- a/openstackquery/enums/props/project_properties.py +++ b/openstackquery/enums/props/project_properties.py @@ -1,4 +1,5 @@ import json +import re from enum import auto from openstackquery.enums.props.prop_enum import PropEnum @@ -20,6 +21,7 @@ class ProjectProperties(PropEnum): PROJECT_NAME = auto() PROJECT_PARENT_ID = auto() PROJECT_TAGS = auto() + PROJECT_EMAIL = auto() @staticmethod def _get_aliases(): @@ -35,8 +37,20 @@ def _get_aliases(): ProjectProperties.PROJECT_NAME: ["name"], ProjectProperties.PROJECT_PARENT_ID: ["parent_id"], ProjectProperties.PROJECT_TAGS: ["tags"], + ProjectProperties.PROJECT_EMAIL: ["email"], } + @staticmethod + def __extract_email_from_tags(tags): + return next( + ( + tag + for tag in tags + if re.match(r"^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$", str(tag)) + ), + None, + ) + @staticmethod def get_prop_mapping(prop): """ @@ -54,6 +68,9 @@ def get_prop_mapping(prop): ProjectProperties.PROJECT_NAME: lambda a: a["name"], ProjectProperties.PROJECT_PARENT_ID: lambda a: a["parent_id"], ProjectProperties.PROJECT_TAGS: lambda a: json.dumps(a.get("tags", [])), + ProjectProperties.PROJECT_EMAIL: lambda a: ProjectProperties.__extract_email_from_tags( + a.get("tags", []) + ), } try: return mapping[prop] diff --git a/openstackquery/mappings/project_mapping.py b/openstackquery/mappings/project_mapping.py index b76774e..0f3bf6f 100644 --- a/openstackquery/mappings/project_mapping.py +++ b/openstackquery/mappings/project_mapping.py @@ -108,11 +108,13 @@ def get_client_side_handler() -> ClientSideHandler: ProjectProperties.PROJECT_NAME, ProjectProperties.PROJECT_DESCRIPTION, ProjectProperties.PROJECT_TAGS, + ProjectProperties.PROJECT_EMAIL, ], QueryPresets.NOT_MATCHES_REGEX: [ ProjectProperties.PROJECT_NAME, ProjectProperties.PROJECT_DESCRIPTION, ProjectProperties.PROJECT_TAGS, + ProjectProperties.PROJECT_EMAIL, ], } ) diff --git a/tests/enums/props/test_project_properties.py b/tests/enums/props/test_project_properties.py index f783ece..fb28d80 100644 --- a/tests/enums/props/test_project_properties.py +++ b/tests/enums/props/test_project_properties.py @@ -26,6 +26,7 @@ (ProjectProperties.PROJECT_NAME, ["project_name", "name"]), (ProjectProperties.PROJECT_PARENT_ID, ["project_parent_id", "parent_id"]), (ProjectProperties.PROJECT_TAGS, ["project_tags", "tags"]), + (ProjectProperties.PROJECT_EMAIL, ["project_email", "email"]), ], ) def test_property_serialization(expected_prop, test_values, property_variant_generator): @@ -60,3 +61,31 @@ def test_get_marker_prop_func(mock_get_prop_mapping): val = ProjectProperties.get_marker_prop_func() mock_get_prop_mapping.assert_called_once_with(ProjectProperties.PROJECT_ID) assert val == mock_get_prop_mapping.return_value + + +@pytest.mark.parametrize( + "project_data, expected_email", + [ + ( + {"tags": ["caso", "someemail@domain.ac.uk", "immutable"]}, + "someemail@domain.ac.uk", + ), + ( + {"tags": ["not-an-email", 123, "second@email.com"]}, + "second@email.com", + ), + ( + {"tags": ["no-email-here", "still-nothing"]}, + None, + ), + ( + {"tags": []}, + None, + ), + ], +) +def test_project_email_property_mapping(project_data, expected_email): + """Test that PROJECT_EMAIL correctly extracts an email from tags.""" + func = ProjectProperties.get_prop_mapping(ProjectProperties.PROJECT_EMAIL) + result = func(project_data) + assert result == expected_email diff --git a/tests/mappings/test_project_mapping.py b/tests/mappings/test_project_mapping.py index 67ab11f..04cb909 100644 --- a/tests/mappings/test_project_mapping.py +++ b/tests/mappings/test_project_mapping.py @@ -111,17 +111,15 @@ def test_client_side_handlers_string(client_side_test_mappings): client side params for string presets """ handler = ProjectMapping.get_client_side_handler() + string_properties = [ + ProjectProperties.PROJECT_NAME, + ProjectProperties.PROJECT_DESCRIPTION, + ProjectProperties.PROJECT_TAGS, + ProjectProperties.PROJECT_EMAIL, + ] mappings = { - QueryPresets.MATCHES_REGEX: [ - ProjectProperties.PROJECT_NAME, - ProjectProperties.PROJECT_DESCRIPTION, - ProjectProperties.PROJECT_TAGS, - ], - QueryPresets.NOT_MATCHES_REGEX: [ - ProjectProperties.PROJECT_NAME, - ProjectProperties.PROJECT_DESCRIPTION, - ProjectProperties.PROJECT_TAGS, - ], + QueryPresets.MATCHES_REGEX: string_properties, + QueryPresets.NOT_MATCHES_REGEX: string_properties, } client_side_test_mappings(handler, mappings)