Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/user_docs/query_docs/PROJECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ A `Project` has the following properties:
| `bool` | "is_enabled" | Indicates whether users can authorize against this project. <br/>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

Expand Down
22 changes: 22 additions & 0 deletions openstackquery/enums/props/project_properties.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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():
Expand All @@ -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):
"""
Expand All @@ -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]
Expand Down
4 changes: 4 additions & 0 deletions openstackquery/mappings/project_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
}
)
32 changes: 31 additions & 1 deletion tests/enums/props/test_project_properties.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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):
Expand Down Expand Up @@ -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
16 changes: 8 additions & 8 deletions tests/mappings/test_project_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down