Skip to content

Commit 829e5d8

Browse files
authored
fix: add creation date and created by to namespaces (#491)
* fix: add creation date and created by to namespaces * switch to plain python property
1 parent 8151202 commit 829e5d8

5 files changed

Lines changed: 64 additions & 27 deletions

File tree

components/renku_data_services/namespace/blueprints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async def _get_namespaces(
188188
name=ns.name,
189189
slug=ns.latest_slug if ns.latest_slug else ns.slug,
190190
created_by=ns.created_by,
191-
creation_date=None, # NOTE: we do not save creation date in the DB
191+
creation_date=ns.creation_date,
192192
namespace_kind=apispec.NamespaceKind(ns.kind.value),
193193
)
194194
for ns in nss

components/renku_data_services/namespace/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class Namespace:
7373
underlying_resource_id: ULID | str # The user or group ID depending on the Namespace kind
7474
latest_slug: str | None = None
7575
name: str | None = None
76+
creation_date: datetime | None = None
7677

7778

7879
@dataclass(frozen=True, eq=True, kw_only=True)

components/renku_data_services/namespace/orm.py

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -72,37 +72,65 @@ class NamespaceORM(BaseORM):
7272
nullable=True,
7373
index=True,
7474
)
75-
user: Mapped[UserORM | None] = relationship(lazy="joined", init=False, repr=False, viewonly=True)
75+
user: Mapped[UserORM | None] = relationship(
76+
lazy="joined", back_populates="namespace", init=False, repr=False, viewonly=True
77+
)
7678

77-
def dump(self) -> models.Namespace:
78-
"""Create a namespace model from the ORM."""
79-
if self.group_id and self.group:
80-
return models.Namespace(
81-
id=self.id,
82-
slug=self.slug,
83-
kind=models.NamespaceKind.group,
84-
created_by=self.group.created_by,
85-
underlying_resource_id=self.group_id,
86-
latest_slug=self.slug,
87-
name=self.group.name,
88-
)
79+
@property
80+
def created_by(self) -> str:
81+
"""User that created this namespace."""
82+
if self.group is not None:
83+
return self.group.created_by
84+
elif self.user_id:
85+
return self.user_id
86+
raise errors.ProgrammingError(
87+
message=f"Found a namespace {self.slug} that has no group or user associated with it."
88+
)
8989

90-
if not self.user or not self.user_id:
91-
raise errors.ProgrammingError(message="Found a namespace that has no group or user associated with it.")
90+
@property
91+
def creation_date(self) -> datetime | None:
92+
"""When this namespace was created."""
93+
return self.group.creation_date if self.group else None
94+
95+
@property
96+
def underlying_resource_id(self) -> str | ULID:
97+
"""Return the id of the underlying resource."""
98+
if self.group_id is not None:
99+
return self.group_id
100+
elif self.user_id is not None:
101+
return self.user_id
102+
103+
raise errors.ProgrammingError(
104+
message=f"Found a namespace {self.slug} that has no group or user associated with it."
105+
)
92106

93-
name = (
94-
f"{self.user.first_name} {self.user.last_name}"
95-
if self.user.first_name and self.user.last_name
96-
else self.user.first_name or self.user.last_name
107+
@property
108+
def name(self) -> str | None:
109+
"""Return the name of the underlying resource."""
110+
if self.group is not None:
111+
return self.group.name
112+
elif self.user is not None:
113+
return (
114+
f"{self.user.first_name} {self.user.last_name}"
115+
if self.user.first_name and self.user.last_name
116+
else self.user.first_name or self.user.last_name
117+
)
118+
raise errors.ProgrammingError(
119+
message=f"Found a namespace {self.slug} that has no group or user associated with it."
97120
)
121+
122+
def dump(self) -> models.Namespace:
123+
"""Create a namespace model from the ORM."""
124+
kind = models.NamespaceKind.group if self.group else models.NamespaceKind.user
98125
return models.Namespace(
99126
id=self.id,
100127
slug=self.slug,
101-
kind=models.NamespaceKind.user,
102-
created_by=self.user_id,
103-
underlying_resource_id=self.user_id,
128+
kind=kind,
129+
created_by=self.created_by,
130+
creation_date=self.creation_date,
131+
underlying_resource_id=self.underlying_resource_id,
104132
latest_slug=self.slug,
105-
name=name,
133+
name=self.name,
106134
)
107135

108136
def dump_user(self) -> UserInfo:
@@ -112,6 +140,8 @@ def dump_user(self) -> UserInfo:
112140
message="Cannot dump ORM namespace as namespace with user if the namespace "
113141
"has no associated user with it."
114142
)
143+
# NOTE: calling `self.user.dump()` can cause sqlalchemy greenlet errors, as it tries to fetch the namespace
144+
# again from the db, even though the back_populates should take care of this and not require loading.
115145
ns = self.dump()
116146
user_info = UserInfo(
117147
id=self.user.keycloak_id,
@@ -154,7 +184,8 @@ def dump(self) -> models.Namespace:
154184
id=self.id,
155185
slug=self.slug,
156186
latest_slug=self.slug,
157-
created_by=self.latest_slug.group.created_by,
187+
created_by=self.latest_slug.created_by,
188+
creation_date=self.created_at,
158189
kind=models.NamespaceKind.group,
159190
underlying_resource_id=self.latest_slug.group_id,
160191
name=self.latest_slug.group.name,
@@ -175,6 +206,7 @@ def dump(self) -> models.Namespace:
175206
slug=self.slug,
176207
latest_slug=self.latest_slug.slug,
177208
created_by=self.latest_slug.user_id,
209+
creation_date=self.created_at,
178210
kind=models.NamespaceKind.user,
179211
underlying_resource_id=self.latest_slug.user_id,
180212
name=name,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ line-length = 120
104104
target-version = "py311"
105105
output-format = "full"
106106
include = ["*.py", "*.pyi"]
107-
exclude = ["*/avro_models/*"]
107+
exclude = ["*/avro_models/*", ".devcontainer/"]
108108

109109
[tool.ruff.format]
110110
exclude = ["apispec.py"]

test/bases/renku_data_services/data_api/test_namespaces.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
@pytest.mark.asyncio
5-
async def test_list_namespaces(sanic_client, user_headers) -> None:
5+
async def test_list_namespaces(sanic_client, user_headers, regular_user) -> None:
66
payload = {
77
"name": "Group1",
88
"slug": "group-1",
@@ -16,8 +16,12 @@ async def test_list_namespaces(sanic_client, user_headers) -> None:
1616
assert len(res_json) == 2
1717
user_ns = res_json[0]
1818
assert user_ns["slug"] == "user.doe"
19+
assert user_ns.get("creation_date") is None
20+
assert user_ns["created_by"] == regular_user.id
1921
group_ns = res_json[1]
2022
assert group_ns["slug"] == "group-1"
23+
assert group_ns.get("creation_date") is not None
24+
assert group_ns["created_by"] == regular_user.id
2125

2226

2327
@pytest.mark.asyncio

0 commit comments

Comments
 (0)