diff --git a/docs/user_docs/query_docs/PROJECTS.md b/docs/user_docs/query_docs/PROJECTS.md index f9396a0..a64f37a 100644 --- a/docs/user_docs/query_docs/PROJECTS.md +++ b/docs/user_docs/query_docs/PROJECTS.md @@ -27,6 +27,8 @@ 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) | +| `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 4b83e05..09b0a86 100644 --- a/openstackquery/enums/props/project_properties.py +++ b/openstackquery/enums/props/project_properties.py @@ -1,4 +1,7 @@ +import json +import re from enum import auto + from openstackquery.enums.props.prop_enum import PropEnum from openstackquery.exceptions.query_property_mapping_error import ( QueryPropertyMappingError, @@ -17,6 +20,8 @@ class ProjectProperties(PropEnum): PROJECT_IS_ENABLED = auto() PROJECT_NAME = auto() PROJECT_PARENT_ID = auto() + PROJECT_TAGS = auto() + PROJECT_EMAIL = auto() @staticmethod def _get_aliases(): @@ -31,8 +36,21 @@ def _get_aliases(): ProjectProperties.PROJECT_IS_ENABLED: ["is_enabled"], 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): """ @@ -49,6 +67,10 @@ 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", [])), + 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 4ce582a..0f3bf6f 100644 --- a/openstackquery/mappings/project_mapping.py +++ b/openstackquery/mappings/project_mapping.py @@ -107,10 +107,14 @@ def get_client_side_handler() -> ClientSideHandler: QueryPresets.MATCHES_REGEX: [ 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 d9786f8..fb28d80 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,8 @@ ), (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): @@ -59,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 c4c3e6f..04cb909 100644 --- a/tests/mappings/test_project_mapping.py +++ b/tests/mappings/test_project_mapping.py @@ -111,15 +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, - ], - QueryPresets.NOT_MATCHES_REGEX: [ - ProjectProperties.PROJECT_NAME, - ProjectProperties.PROJECT_DESCRIPTION, - ], + QueryPresets.MATCHES_REGEX: string_properties, + QueryPresets.NOT_MATCHES_REGEX: string_properties, } client_side_test_mappings(handler, mappings)