Skip to content

feat: add RSS data models (SQLAlchemy + Pydantic)#857

Draft
HeloiseJoffe wants to merge 4 commits intoDIRACGrid:mainfrom
HeloiseJoffe:feat/rss-data-models
Draft

feat: add RSS data models (SQLAlchemy + Pydantic)#857
HeloiseJoffe wants to merge 4 commits intoDIRACGrid:mainfrom
HeloiseJoffe:feat/rss-data-models

Conversation

@HeloiseJoffe
Copy link
Copy Markdown
Contributor

Closes #837

Changes :

  • Add RSS SQLAlchemy 2.0 models for the tables and ResourceStatusDB class
  • Add RSS entry point
  • Add pydantic models and ResourceType enum
  • Add Unit tests

@HeloiseJoffe HeloiseJoffe requested a review from aldbr March 25, 2026 09:33
@fstagni fstagni closed this Mar 26, 2026
@fstagni fstagni reopened this Mar 26, 2026
@read-the-docs-community
Copy link
Copy Markdown

read-the-docs-community bot commented Mar 26, 2026

Documentation build overview

📚 diracx | 🛠️ Build #32165038 | 📁 Comparing 5c3f59d against latest (63dc086)

  🔍 Preview build  

No files changed.

@aldbr aldbr force-pushed the feat/rss-data-models branch from dee80c6 to bf5abcb Compare March 26, 2026 18:26
@HeloiseJoffe HeloiseJoffe force-pushed the feat/rss-data-models branch from 59514d8 to 036cb9c Compare March 30, 2026 08:35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now db.py does three things:

  • queries the database
  • dispatches based on resource type
  • converts DB strings into Pydantic models via map_status().

In diracx's architecture (routers -> logic -> db -> core), we try to follow a n-layer architecture approach, where each layer has a single responsibility. I think the DB layer should only do the first one and should be generic and simple.

See https://diracx.diracgrid.org/en/latest/dev/explanations/components/#python-modules

I would likely have 2 methods here: one to query the sites and one to query the resources.
Then the logic would call one of these 2 methods, and would return the right pydantic model based on the result (so it would likely have 4 functions: site, storage, compute, fts).

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I think that's actually better. I've organized the features according to the diracx's architecture.

@DIRACGridBot DIRACGridBot marked this pull request as draft March 31, 2026 08:59
@HeloiseJoffe HeloiseJoffe force-pushed the feat/rss-data-models branch 2 times, most recently from cb53ac5 to 87c7b68 Compare April 2, 2026 06:37
@HeloiseJoffe HeloiseJoffe marked this pull request as ready for review April 2, 2026 06:39
@HeloiseJoffe HeloiseJoffe force-pushed the feat/rss-data-models branch from 87c7b68 to 5c3f59d Compare April 8, 2026 09:29
@HeloiseJoffe HeloiseJoffe requested a review from aldbr April 8, 2026 12:06
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about calling that file query.py?

Comment on lines +60 to +73
def map_status(db_status: str, reason: str | None = None) -> ResourceStatus:
if db_status in ALLOWED:
return AllowedStatus(allowed=True)

if db_status in BANNED:
return BannedStatus(
allowed=False,
reason=reason or db_status,
)

return BannedStatus(
allowed=False,
reason=f"Unknown status: {db_status}",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about moving map_status to diracx-logic?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And therefore you can potentially move that test to diracx-logic too



class ElementStatusBase:
__table_args__ = {"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need that?

Suggested change
__table_args__ = {"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"}

vo: Mapped[str64] = mapped_column("VO", primary_key=True, server_default="all")
status: Mapped[str] = mapped_column("Status", String(8), server_default="")
reason: Mapped[str512] = mapped_column("Reason", server_default="Unspecified")
dateeffective: Mapped[datetime] = mapped_column("DateEffective", DateTime)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you need to pass SmarterDateTime here? Same for the other dates.

Suggested change
dateeffective: Mapped[datetime] = mapped_column("DateEffective", DateTime)
date_effective: Mapped[datetime] = mapped_column("DateEffective", SmarterDateTime())

"ID", BigInteger, autoincrement=True, primary_key=True
)
name: Mapped[str64] = mapped_column("Name")
statustype: Mapped[str128] = mapped_column("StatusType", server_default="all")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for not using snake case as in the other table definitions? Same for all the attribute names

Suggested change
statustype: Mapped[str128] = mapped_column("StatusType", server_default="all")
status_type: Mapped[str128] = mapped_column("StatusType", server_default="all")


def map_status(db_status: str, reason: str | None = None) -> ResourceStatus:
if db_status in ALLOWED:
return AllowedStatus(allowed=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the warnings?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The should be what is in reason, at least for the case when db_status is "Degraded".

)

# Test with the test Site (should be found)
async with rss_db as rss_db:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: to avoid name shadowing

Suggested change
async with rss_db as rss_db:
async with rss_db as db:


metadata = RSSBase.metadata

async def get_site_status(self, name: str, vo: str = "all") -> tuple[str, str]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fstagni out of curiosity, shouldn't we (not now but later) transition from this magic all to NULL in the DB?
If not VO is set, then we assume it's for all of them.
Unless I miss something?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree with you

async def get_resource_status(
self,
name: str,
statustypes: list[str] = ["all"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beware of https://docs.astral.sh/ruff/rules/mutable-argument-default/
It's fine here because we don't mutate status_types but we should be careful.

@fstagni same here, should we transition from all for VO and StatusType to NULL at some point?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree to this one too.

@DIRACGridBot DIRACGridBot marked this pull request as draft April 9, 2026 16:36

def map_status(db_status: str, reason: str | None = None) -> ResourceStatus:
if db_status in ALLOWED:
return AllowedStatus(allowed=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The should be what is in reason, at least for the case when db_status is "Degraded".


metadata = RSSBase.metadata

async def get_site_status(self, name: str, vo: str = "all") -> tuple[str, str]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree with you

async def get_resource_status(
self,
name: str,
statustypes: list[str] = ["all"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree to this one too.

)


class ResourceNotFoundError(DiracError):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am really wondering if we need this one at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RSS data models (SQLAlchemy + Pydantic)

3 participants