Coverage for pepdbagent/modules/schema.py: 82%
219 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-01 22:07 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-01 22:07 +0000
1import logging
3from sqlalchemy import Select, and_, func, or_, select
4from sqlalchemy.orm import Session
5from sqlalchemy.orm.attributes import flag_modified
7from pepdbagent.const import DEFAULT_TAG_VERSION, LATEST_SCHEMA_VERSION, PKG_NAME
8from pepdbagent.db_utils import (
9 BaseEngine,
10 SchemaRecords,
11 SchemaTags,
12 SchemaVersions,
13 User,
14)
15from pepdbagent.exceptions import (
16 SchemaAlreadyExistsError,
17 SchemaDoesNotExistError,
18 SchemaTagAlreadyExistsError,
19 SchemaTagDoesNotExistError,
20 SchemaVersionAlreadyExistsError,
21 SchemaVersionDoesNotExistError,
22)
23from pepdbagent.models import (
24 PaginationResult,
25 SchemaRecordAnnotation,
26 SchemaSearchResult,
27 SchemaVersionAnnotation,
28 SchemaVersionSearchResult,
29 UpdateSchemaRecordFields,
30 UpdateSchemaVersionFields,
31)
33_LOGGER = logging.getLogger(PKG_NAME)
36class PEPDatabaseSchema:
37 """
38 Class that represents SchemaRecords in Database.
40 While using this class, user can create, retrieve, delete, and update schemas from database
41 """
43 def __init__(self, pep_db_engine: BaseEngine):
44 """
45 Args:
46 pep_db_engine: PEPDatabaseAgent engine object.
47 """
48 self._sa_engine = pep_db_engine.engine
49 self._pep_db_engine = pep_db_engine
51 def get(self, namespace: str, name: str, version: str) -> dict:
52 """Get a schema value from the database.
54 Args:
55 namespace: User namespace.
56 name: Schema name.
57 version: Schema version (use "latest" for the most recent).
59 Returns:
60 Schema value dict.
62 Raises:
63 SchemaVersionDoesNotExistError: If the schema or version does not exist.
64 """
66 with Session(self._sa_engine) as session:
67 if version == LATEST_SCHEMA_VERSION:
68 schema_obj = session.scalar(
69 select(SchemaVersions)
70 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
71 .where(
72 and_(
73 SchemaRecords.namespace == namespace,
74 SchemaRecords.name == name,
75 )
76 )
77 .order_by(SchemaVersions.version.desc())
78 )
80 else:
81 schema_obj = session.scalar(
82 select(SchemaVersions)
83 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
84 .where(
85 and_(
86 SchemaRecords.namespace == namespace,
87 SchemaRecords.name == name,
88 SchemaVersions.version == version,
89 )
90 )
91 )
93 if not schema_obj:
94 raise SchemaVersionDoesNotExistError(
95 f"Schema '{name}' does not exist in the database"
96 )
98 return schema_obj.schema_value
100 def create(
101 self,
102 namespace: str,
103 name: str,
104 schema_value: dict,
105 version: str = DEFAULT_TAG_VERSION,
106 description: str = "",
107 lifecycle_stage: str = "",
108 maintainers: str = "",
109 contributors: str = "",
110 release_notes: str = "",
111 tags: list[str] | str | dict[str, str] | list[dict[str, str]] | None = None,
112 private: bool = False, # TODO: for simplicity was not implemented yet
113 ) -> None:
114 """Create a new schema record in the database.
116 Args:
117 namespace: User namespace.
118 name: Schema name.
119 schema_value: Schema content as a dict.
120 version: Initial version string.
121 description: Schema description.
122 lifecycle_stage: Lifecycle stage, e.g., "stable" or "deprecated".
123 maintainers: Comma-separated maintainer names.
124 contributors: Comma-separated contributor names.
125 release_notes: Release notes for this version.
126 tags: Tags to associate with the schema version.
127 private: Mark schema as private if True.
129 Raises:
130 SchemaAlreadyExistsError: If a schema with the same name exists in the namespace.
131 """
133 tags = self._unify_tags(tags)
135 with Session(self._sa_engine) as session:
136 schema_obj = session.scalar(
137 select(SchemaRecords).where(
138 and_(
139 SchemaRecords.namespace == namespace, SchemaRecords.name == name
140 )
141 )
142 )
144 if schema_obj:
145 raise SchemaAlreadyExistsError(
146 f"Schema '{name}' already exists in the database"
147 )
149 user = session.scalar(select(User).where(User.namespace == namespace))
151 if not user:
152 user = User(namespace=namespace)
153 session.add(user)
154 session.commit()
156 user.number_of_schemas += 1
158 schema_obj = SchemaRecords(
159 namespace=namespace,
160 name=name,
161 description=description,
162 maintainers=maintainers,
163 lifecycle_stage=lifecycle_stage,
164 private=private,
165 )
167 session.add(schema_obj)
169 schema_version_obj = SchemaVersions(
170 schema_mapping=schema_obj,
171 version=version,
172 schema_value=schema_value,
173 release_notes=release_notes,
174 contributors=contributors,
175 )
177 for tag_name, tag_value in tags.items():
178 tag_obj = session.scalar(
179 select(SchemaTags).where(SchemaTags.tag_name == tag_name)
180 )
181 if not tag_obj:
182 tag_obj = SchemaTags(
183 tag_name=tag_name,
184 tag_value=tag_value,
185 schema_mapping=schema_version_obj,
186 )
187 session.add(tag_obj)
189 session.add(schema_version_obj)
190 session.commit()
192 return None
194 def add_version(
195 self,
196 namespace: str,
197 name: str,
198 version: str,
199 schema_value: dict,
200 release_notes: str = "",
201 contributors: str = "",
202 overwrite: bool = False,
203 tags: list[str] | str | dict[str, str] | list[dict[str, str]] | None = None,
204 ) -> None:
206 tags = self._unify_tags(tags)
208 with Session(self._sa_engine) as session:
209 schema_obj = session.scalar(
210 select(SchemaRecords).where(
211 and_(
212 SchemaRecords.namespace == namespace, SchemaRecords.name == name
213 )
214 )
215 )
216 if not schema_obj:
217 raise SchemaDoesNotExistError(
218 f"Schema '{name}' does not exist in the database. Unable to add version."
219 )
221 version_obj = session.scalar(
222 select(SchemaVersions)
223 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
224 .where(
225 and_(
226 SchemaRecords.namespace == namespace,
227 SchemaRecords.name == name,
228 SchemaVersions.version == version,
229 )
230 )
231 )
233 if version_obj:
234 if not overwrite:
235 raise SchemaVersionAlreadyExistsError(
236 f"Schema '{name}' with version '{version}' already exists in the database"
237 )
239 return self.update_schema_version(
240 namespace,
241 name,
242 version,
243 update_fields=UpdateSchemaVersionFields(
244 schema_value=schema_value,
245 release_notes=release_notes,
246 contributors=contributors,
247 ),
248 )
250 schema_obj.last_update_date = func.now()
252 schema_version_obj = SchemaVersions(
253 schema_id=schema_obj.id,
254 version=version,
255 schema_value=schema_value,
256 release_notes=release_notes,
257 contributors=contributors,
258 )
260 for tag_name, tag_value in tags.items():
261 tag_obj = SchemaTags(
262 tag_name=tag_name,
263 tag_value=tag_value,
264 schema_mapping=schema_version_obj,
265 )
266 session.add(tag_obj)
268 session.add(schema_version_obj)
269 session.commit()
271 return None
273 def update_schema_version(
274 self,
275 namespace: str,
276 name: str,
277 version: str,
278 update_fields: UpdateSchemaVersionFields | dict,
279 ) -> None:
280 """Update fields of an existing schema version.
282 Args:
283 namespace: User namespace.
284 name: Schema name.
285 version: Schema version to update.
286 update_fields: Fields to update — contributors, schema_value, and/or release_notes.
288 Raises:
289 SchemaVersionDoesNotExistError: If the version does not exist.
290 """
291 if isinstance(update_fields, dict):
292 update_fields = UpdateSchemaVersionFields(**update_fields)
293 update_fields = update_fields.model_dump(
294 exclude_unset=True, exclude_defaults=True
295 )
297 with Session(self._sa_engine) as session:
298 schema_obj = session.scalar(
299 select(SchemaVersions)
300 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
301 .where(
302 and_(
303 SchemaRecords.namespace == namespace,
304 SchemaRecords.name == name,
305 SchemaVersions.version == version,
306 )
307 )
308 )
310 if not schema_obj:
311 raise SchemaVersionDoesNotExistError(
312 f"Schema '{name}' with version '{version}' does not exist in the database. Unable to update version."
313 )
314 schema_obj.last_update_date = func.now()
316 for field, value in update_fields.items():
317 setattr(schema_obj, field, value)
318 if field == "schema_value":
319 flag_modified(schema_obj, field)
321 session.commit()
323 def update_schema_record(
324 self,
325 namespace: str,
326 name: str,
327 update_fields: UpdateSchemaRecordFields | dict,
328 ) -> None:
329 """Update metadata fields of a schema record.
331 Args:
332 namespace: User namespace.
333 name: Schema name.
334 update_fields: Fields to update — maintainers, lifecycle_stage, private, and/or name.
336 Raises:
337 SchemaDoesNotExistError: If the schema does not exist.
338 """
340 if isinstance(update_fields, dict):
341 update_fields = UpdateSchemaRecordFields(**update_fields)
343 update_fields = update_fields.model_dump(
344 exclude_unset=True, exclude_defaults=True
345 )
347 with Session(self._sa_engine) as session:
348 schema_obj = session.scalar(
349 select(SchemaRecords).where(
350 and_(
351 SchemaRecords.namespace == namespace, SchemaRecords.name == name
352 )
353 )
354 )
356 if not schema_obj:
357 raise SchemaDoesNotExistError(
358 f"Schema '{name}' does not exist in the database"
359 )
361 for field, value in update_fields.items():
362 setattr(schema_obj, field, value)
364 session.commit()
366 def schema_exist(self, namespace: str, name: str) -> bool:
367 """Check whether a schema exists in the database.
369 Args:
370 namespace: User namespace.
371 name: Schema name.
373 Returns:
374 True if the schema exists.
375 """
377 with Session(self._sa_engine) as session:
378 schema_obj = session.scalar(
379 select(SchemaRecords).where(
380 and_(
381 SchemaRecords.namespace == namespace, SchemaRecords.name == name
382 )
383 )
384 )
385 return True if schema_obj else False
387 def version_exist(self, namespace: str, name: str, version: str) -> bool:
388 """Check whether a specific schema version exists.
390 Args:
391 namespace: User namespace.
392 name: Schema name.
393 version: Schema version.
395 Returns:
396 True if the version exists.
397 """
399 with Session(self._sa_engine) as session:
400 schema_obj = session.scalar(
401 select(SchemaVersions)
402 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
403 .where(
404 and_(
405 SchemaRecords.namespace == namespace,
406 SchemaRecords.name == name,
407 SchemaVersions.version == version,
408 )
409 )
410 )
411 return True if schema_obj else False
413 def get_schema_info(self, namespace: str, name: str) -> SchemaRecordAnnotation:
414 """Get metadata for a schema record.
416 Args:
417 namespace: User namespace.
418 name: Schema name.
420 Returns:
421 SchemaRecordAnnotation with schema metadata.
423 Raises:
424 SchemaDoesNotExistError: If the schema does not exist.
425 """
427 with Session(self._sa_engine) as session:
428 schema_obj = session.scalar(
429 select(SchemaRecords).where(
430 and_(
431 SchemaRecords.namespace == namespace, SchemaRecords.name == name
432 )
433 )
434 )
436 if not schema_obj:
437 raise SchemaDoesNotExistError(
438 f"Schema '{name}' does not exist in the database"
439 )
441 return SchemaRecordAnnotation(
442 namespace=schema_obj.namespace,
443 schema_name=schema_obj.name,
444 description=schema_obj.description,
445 latest_released_version=schema_obj.versions_mapping[0].version,
446 maintainers=schema_obj.maintainers,
447 private=schema_obj.private,
448 last_update_date=schema_obj.last_update_date,
449 lifecycle_stage=schema_obj.lifecycle_stage,
450 )
452 def get_version_info(
453 self, namespace: str, name: str, version: str
454 ) -> SchemaVersionAnnotation:
455 """Get metadata for a specific schema version.
457 Args:
458 namespace: User namespace.
459 name: Schema name.
460 version: Schema version (use "latest" for the most recent).
462 Returns:
463 SchemaVersionAnnotation with version metadata and tags.
465 Raises:
466 SchemaVersionDoesNotExistError: If the version does not exist.
467 """
469 with Session(self._sa_engine) as session:
470 # if user provided "latest" version
471 if version == LATEST_SCHEMA_VERSION:
472 version_obj = session.scalar(
473 select(SchemaVersions)
474 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
475 .where(
476 and_(
477 SchemaRecords.namespace == namespace,
478 SchemaRecords.name == name,
479 )
480 )
481 .order_by(SchemaVersions.version.desc())
482 .limit(1)
483 )
484 else:
485 version_obj = session.scalar(
486 select(SchemaVersions)
487 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
488 .where(
489 and_(
490 SchemaRecords.namespace == namespace,
491 SchemaRecords.name == name,
492 SchemaVersions.version == version,
493 )
494 )
495 )
497 if not version_obj:
498 raise SchemaVersionDoesNotExistError(
499 f"Schema '{name}' with version '{version}' does not exist in the database"
500 )
502 return SchemaVersionAnnotation(
503 namespace=version_obj.schema_mapping.namespace,
504 schema_name=version_obj.schema_mapping.name,
505 version=version_obj.version,
506 contributors=version_obj.contributors,
507 release_notes=version_obj.release_notes,
508 tags={tag.tag_name: tag.tag_value for tag in version_obj.tags_mapping},
509 release_date=version_obj.release_date,
510 last_update_date=version_obj.last_update_date,
511 )
513 def fetch_schemas(
514 self,
515 namespace: str = None,
516 name: str = None,
517 maintainer: str = None,
518 lifecycle_stage: str = None,
519 latest_version: str = None,
520 page: int = 0,
521 page_size: int = 10,
522 order_by: str = "update_date",
523 order_desc: bool = False,
524 ) -> SchemaSearchResult:
525 """Get schemas matching the given filters.
527 Returns all schemas if no filters are provided.
529 Args:
530 namespace: Restrict to this namespace (default: all namespaces).
531 name: Partial name match.
532 maintainer: Partial maintainer match.
533 lifecycle_stage: Partial lifecycle stage match.
534 latest_version: Partial latest version match.
535 page: Page number (zero-based).
536 page_size: Number of results per page.
537 order_by: Sort field — "name" or "update_date".
538 order_desc: Sort in descending order if True.
540 Returns:
541 SchemaSearchResult with pagination and list of SchemaRecordAnnotation objects.
542 """
544 # filters = [
545 # SchemaRecords.namespace == namespace if namespace else None,
546 # SchemaRecords.name == name if name else None,
547 # SchemaRecords.maintainers == maintainer if maintainer else None,
548 # SchemaRecords.lifecycle_stage == lifecycle_stage if lifecycle_stage else None,
549 # ]
550 filters = [
551 SchemaRecords.namespace.ilike(f"%{namespace}%") if namespace else None,
552 SchemaRecords.name.ilike(f"%{name}%") if name else None,
553 SchemaRecords.maintainers.ilike(f"%{maintainer}%") if maintainer else None,
554 (
555 SchemaRecords.lifecycle_stage.ilike(f"%{lifecycle_stage}%")
556 if lifecycle_stage
557 else None
558 ),
559 ]
561 # Remove None values before applying and_
562 conditions = [f for f in filters if f is not None]
564 statement = (
565 select(SchemaRecords).where(and_(*conditions))
566 if conditions
567 else select(SchemaRecords)
568 )
569 statement_count = (
570 select(func.count(SchemaRecords.id)).where(and_(*conditions))
571 if conditions
572 else select(func.count(SchemaRecords.id))
573 )
575 with Session(self._sa_engine) as session:
576 total = session.scalar(statement_count)
578 statement = self._add_order_by_schemas_keyword(
579 statement, by=order_by, desc=order_desc
580 )
582 results_objects = session.scalars(
583 statement.limit(page_size).offset(page * page_size)
584 )
585 return SchemaSearchResult(
586 pagination=PaginationResult(
587 page=page,
588 page_size=page_size,
589 total=total,
590 ),
591 results=[
592 SchemaRecordAnnotation(
593 namespace=result.namespace,
594 schema_name=result.name,
595 latest_released_version=result.versions_mapping[0].version,
596 description=result.description,
597 maintainers=result.maintainers,
598 private=result.private,
599 last_update_date=result.last_update_date,
600 )
601 for result in results_objects
602 ],
603 )
605 def query_schemas(
606 self,
607 namespace: str = None,
608 search_str: str = "",
609 page: int = 0,
610 page_size: int = 10,
611 order_by: str = "update_date",
612 order_desc: bool = False,
613 ) -> SchemaSearchResult:
614 """Search schemas by name and description with pagination.
616 Args:
617 namespace: Restrict to this namespace (default: all namespaces).
618 search_str: Text to search in name and description (default: all schemas).
619 page: Page number (zero-based).
620 page_size: Number of results per page.
621 order_by: Sort field — "name" or "update_date".
622 order_desc: Sort in descending order if True.
624 Returns:
625 SchemaSearchResult with pagination and list of SchemaRecordAnnotation objects.
626 """
628 search_str = search_str.lower() if search_str else ""
630 where_statement = or_(
631 SchemaRecords.name.ilike(f"%{search_str}%"),
632 SchemaRecords.description.ilike(f"%{search_str}%"),
633 )
634 if namespace:
635 where_statement = and_(
636 where_statement, SchemaRecords.namespace == namespace
637 )
639 with Session(self._sa_engine) as session:
640 total = session.scalar(
641 select(func.count(SchemaRecords.id)).where(where_statement)
642 )
643 statement = (
644 select(SchemaRecords)
645 .where(where_statement)
646 .limit(page_size)
647 .offset(page * page_size)
648 )
649 statement = self._add_order_by_schemas_keyword(
650 statement, by=order_by, desc=order_desc
651 )
652 results_objects = session.scalars(statement)
654 return SchemaSearchResult(
655 pagination=PaginationResult(
656 page=page,
657 page_size=page_size,
658 total=total,
659 ),
660 results=[
661 SchemaRecordAnnotation(
662 namespace=result.namespace,
663 schema_name=result.name,
664 latest_released_version=result.versions_mapping[0].version,
665 description=result.description,
666 maintainers=result.maintainers,
667 private=result.private,
668 last_update_date=result.last_update_date,
669 )
670 for result in results_objects
671 ],
672 )
674 def query_schema_version(
675 self,
676 namespace: str,
677 name: str,
678 tag: str = None,
679 search_str: str = "",
680 page: int = 0,
681 page_size: int = 10,
682 ) -> SchemaVersionSearchResult:
683 """Search versions of a schema with pagination.
685 Args:
686 namespace: User namespace.
687 name: Schema name.
688 tag: Filter by tag name (default: all tags).
689 search_str: Text to search in version and release_notes.
690 page: Page number (zero-based).
691 page_size: Number of results per page.
693 Returns:
694 SchemaVersionSearchResult with pagination and list of SchemaVersionAnnotation objects.
696 Raises:
697 SchemaDoesNotExistError: If the schema does not exist.
698 """
700 search_str = search_str.lower() if search_str else ""
702 with Session(self._sa_engine) as session:
703 schema_obj = session.scalar(
704 select(SchemaRecords).where(
705 and_(
706 SchemaRecords.namespace == namespace, SchemaRecords.name == name
707 )
708 )
709 )
711 if not schema_obj:
712 raise SchemaDoesNotExistError(
713 f"Schema '{name}' does not exist in the database"
714 )
716 where_statement = and_(
717 SchemaRecords.namespace == namespace,
718 SchemaRecords.name == name,
719 or_(
720 SchemaVersions.version.ilike(f"%{search_str}%"),
721 SchemaVersions.release_notes.ilike(f"%{search_str}%"),
722 ),
723 )
725 if tag:
726 where_statement = and_(where_statement, SchemaTags.tag_name == tag)
727 total_statement = (
728 select(func.count(SchemaVersions.id))
729 .join(SchemaRecords)
730 .join(SchemaTags)
731 .where(where_statement)
732 )
733 find_statement = (
734 select(SchemaVersions)
735 .join(SchemaRecords)
736 .join(SchemaTags)
737 .where(where_statement)
738 )
740 else:
741 total_statement = (
742 select(func.count(SchemaVersions.id))
743 .join(SchemaRecords)
744 .where(where_statement)
745 )
746 find_statement = (
747 select(SchemaVersions).join(SchemaRecords).where(where_statement)
748 )
750 total = session.scalar(total_statement)
752 results_objects = session.scalars(
753 find_statement.order_by(SchemaVersions.version.desc())
754 .limit(page_size)
755 .offset(page * page_size)
756 ).unique()
758 return SchemaVersionSearchResult(
759 pagination=PaginationResult(
760 page=page,
761 page_size=page_size,
762 total=total,
763 ),
764 results=[
765 SchemaVersionAnnotation(
766 namespace=result.schema_mapping.namespace,
767 schema_name=result.schema_mapping.name,
768 version=result.version,
769 contributors=result.contributors,
770 release_notes=result.release_notes,
771 tags={
772 tag.tag_name: tag.tag_value for tag in result.tags_mapping
773 },
774 release_date=result.release_date,
775 last_update_date=result.last_update_date,
776 )
777 for result in results_objects
778 ],
779 )
781 def delete_schema(self, namespace: str, name: str) -> None:
782 """Delete a schema record and all its versions from the database.
784 Args:
785 namespace: User namespace.
786 name: Schema name.
788 Raises:
789 SchemaDoesNotExistError: If the schema does not exist.
790 """
792 with Session(self._sa_engine) as session:
793 schema_obj = session.scalar(
794 select(SchemaRecords).where(
795 and_(
796 SchemaRecords.namespace == namespace, SchemaRecords.name == name
797 )
798 )
799 )
801 if not schema_obj:
802 raise SchemaDoesNotExistError(
803 f"Schema '{name}' does not exist in the database"
804 )
806 statement = select(User).where(User.namespace == namespace)
807 user = session.scalar(statement)
808 if user:
809 user.number_of_schemas -= 1
810 session.commit()
812 session.delete(schema_obj)
813 session.commit()
815 def delete_version(self, namespace: str, name: str, version: str) -> None:
816 """Delete a specific schema version.
818 Args:
819 namespace: Namespace of the schema.
820 name: Name of the schema.
821 version: Version to delete.
823 Raises:
824 SchemaVersionDoesNotExistError: If the version does not exist.
825 """
826 with Session(self._sa_engine) as session:
827 schema_obj = session.scalar(
828 select(SchemaVersions)
829 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
830 .where(
831 and_(
832 SchemaRecords.namespace == namespace,
833 SchemaRecords.name == name,
834 SchemaVersions.version == version,
835 )
836 )
837 )
838 if not schema_obj:
839 raise SchemaVersionDoesNotExistError(
840 f"Schema '{name}' with version '{version}' does not exist in the database. Unable to update version."
841 )
843 session.delete(schema_obj)
844 session.commit()
846 def add_tag_to_schema(
847 self,
848 namespace: str,
849 name: str,
850 version: str,
851 tag: list[str] | str | dict[str, str] | None,
852 ) -> None:
853 """Add a tag to a schema version.
855 Args:
856 namespace: Namespace of the schema.
857 name: Name of the schema.
858 version: Schema version.
859 tag: Tag to add — string, list of strings, or dict of name→value pairs.
861 Raises:
862 SchemaVersionDoesNotExistError: If the version does not exist.
863 SchemaTagAlreadyExistsError: If the tag already exists on the version.
864 """
866 tag = self._unify_tags(tag)
868 with Session(self._sa_engine) as session:
869 schema_obj = session.scalar(
870 select(SchemaVersions)
871 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
872 .where(
873 and_(
874 SchemaRecords.namespace == namespace,
875 SchemaRecords.name == name,
876 SchemaVersions.version == version,
877 )
878 )
879 )
880 if not schema_obj:
881 raise SchemaVersionDoesNotExistError(
882 f"Schema '{name}' with version '{version}' does not exist in the database. Unable to add tag."
883 )
884 if isinstance(tag, str):
885 tag = [tag]
887 for tag_name, tag_value in tag.items():
888 tag_obj = session.scalar(
889 select(SchemaTags).where(SchemaTags.tag_name == tag_name)
890 )
891 if not tag_obj:
892 tag_obj = SchemaTags(
893 tag_name=tag_name,
894 tag_value=tag_value,
895 schema_mapping=schema_obj,
896 )
897 session.add(tag_obj)
898 else:
899 raise SchemaTagAlreadyExistsError(
900 f"Tag '{tag_name}' already exists in the schema"
901 )
903 session.commit()
905 def remove_tag_from_schema(
906 self, namespace: str, name: str, version: str, tag: str
907 ) -> None:
908 """Remove a tag from a schema version.
910 Args:
911 namespace: Namespace of the schema.
912 name: Name of the schema.
913 version: Schema version.
914 tag: Name of the tag to remove.
916 Raises:
917 SchemaVersionDoesNotExistError: If the version does not exist.
918 SchemaTagDoesNotExistError: If the tag does not exist on the version.
919 """
920 with Session(self._sa_engine) as session:
921 schema_obj = session.scalar(
922 select(SchemaVersions)
923 .join(SchemaRecords, SchemaRecords.id == SchemaVersions.schema_id)
924 .where(
925 and_(
926 SchemaRecords.namespace == namespace,
927 SchemaRecords.name == name,
928 SchemaVersions.version == version,
929 )
930 )
931 )
932 if not schema_obj:
933 raise SchemaVersionDoesNotExistError(
934 f"Schema '{name}' with version '{version}' does not exist in the database. Unable to remove tag."
935 )
937 tag_obj = session.scalar(
938 select(SchemaTags).where(
939 SchemaTags.tag_name == tag,
940 SchemaTags.schema_version_id == schema_obj.id,
941 )
942 )
943 if not tag_obj:
944 raise SchemaTagDoesNotExistError(
945 f"Tag '{tag}' does not exist in the schema"
946 )
948 session.delete(tag_obj)
949 session.commit()
951 @staticmethod
952 def _add_order_by_schemas_keyword(
953 statement: Select, by: str = "update_date", desc: bool = False
954 ) -> Select:
955 """Add an ORDER BY clause for schema queries.
957 Args:
958 statement: SQLAlchemy SELECT statement to augment.
959 by: Sort field — "name" or "update_date".
960 desc: Sort in descending order if True.
962 Returns:
963 Statement with ORDER BY applied.
964 """
965 if by == "update_date":
966 order_by_obj = SchemaRecords.last_update_date
967 elif by == "name":
968 order_by_obj = SchemaRecords.name
969 else:
970 _LOGGER.warning(
971 f"order by: '{by}' statement is unavailable. Projects are sorted by 'update_date'"
972 )
973 order_by_obj = SchemaRecords.last_update_date
975 if desc and by == "name":
976 order_by_obj = order_by_obj.desc()
978 elif by != "name" and not desc:
979 order_by_obj = order_by_obj.desc()
981 return statement.order_by(order_by_obj)
983 def _unify_tags(
984 self, tags: list[str] | str | dict[str, str] | list[dict[str, str]] | None
985 ) -> dict[str, str]:
986 """Normalise tags to a dict[str, str] representation.
988 Args:
989 tags: Tags as a string, list of strings, dict, or list of dicts.
991 Returns:
992 Dict mapping tag names to tag values.
994 Raises:
995 ValueError: If tags are in an unsupported format.
996 """
997 if not tags:
998 tags = {}
999 if tags == (None,):
1000 tags = {}
1001 elif isinstance(tags, str):
1002 tags = {tags: None}
1003 elif isinstance(tags, dict):
1004 pass
1005 elif isinstance(tags, list):
1006 if all(isinstance(tag, str) for tag in tags):
1007 tags = {tag: None for tag in tags}
1008 else:
1009 raise ValueError(
1010 f"tags should be a list of strings or a list of dictionaries. Tag values: {tags}"
1011 )
1012 else:
1013 raise ValueError(
1014 f"tags should be a list of strings or a list of dictionaries. Tag values: {tags}"
1015 )
1016 return tags