pycharter 0.0.20__py3-none-any.whl → 0.0.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. api/dependencies/__init__.py +2 -1
  2. api/dependencies/database.py +71 -5
  3. api/main.py +47 -8
  4. api/models/contracts.py +6 -4
  5. api/models/metadata.py +11 -7
  6. api/models/schemas.py +16 -10
  7. api/routes/v1/contracts.py +498 -226
  8. api/routes/v1/metadata.py +52 -211
  9. api/routes/v1/schemas.py +1 -1
  10. api/routes/v1/settings.py +88 -1
  11. api/utils.py +224 -0
  12. pycharter/__init__.py +149 -93
  13. pycharter/data/templates/template_transform_advanced.yaml +50 -0
  14. pycharter/data/templates/template_transform_simple.yaml +59 -0
  15. pycharter/db/models/base.py +1 -2
  16. pycharter/etl_generator/orchestrator.py +463 -487
  17. pycharter/metadata_store/postgres.py +16 -191
  18. pycharter/metadata_store/sqlite.py +12 -41
  19. {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/METADATA +284 -62
  20. pycharter-0.0.22.dist-info/RECORD +358 -0
  21. ui/static/404/index.html +1 -1
  22. ui/static/404.html +1 -1
  23. ui/static/__next.__PAGE__.txt +1 -1
  24. ui/static/__next._full.txt +2 -2
  25. ui/static/__next._head.txt +1 -1
  26. ui/static/__next._index.txt +2 -2
  27. ui/static/__next._tree.txt +2 -2
  28. ui/static/_next/static/chunks/13d4a0fbd74c1ee4.js +1 -0
  29. ui/static/_next/static/chunks/2edb43b48432ac04.js +441 -0
  30. ui/static/_next/static/chunks/c4fa4f4114b7c352.js +1 -0
  31. ui/static/_next/static/chunks/d2363397e1b2bcab.css +1 -0
  32. ui/static/_next/static/chunks/f7d1a90dd75d2572.js +1 -0
  33. ui/static/_not-found/__next._full.txt +2 -2
  34. ui/static/_not-found/__next._head.txt +1 -1
  35. ui/static/_not-found/__next._index.txt +2 -2
  36. ui/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
  37. ui/static/_not-found/__next._not-found.txt +1 -1
  38. ui/static/_not-found/__next._tree.txt +2 -2
  39. ui/static/_not-found/index.html +1 -1
  40. ui/static/_not-found/index.txt +2 -2
  41. ui/static/contracts/__next._full.txt +3 -3
  42. ui/static/contracts/__next._head.txt +1 -1
  43. ui/static/contracts/__next._index.txt +2 -2
  44. ui/static/contracts/__next._tree.txt +2 -2
  45. ui/static/contracts/__next.contracts.__PAGE__.txt +2 -2
  46. ui/static/contracts/__next.contracts.txt +1 -1
  47. ui/static/contracts/index.html +1 -1
  48. ui/static/contracts/index.txt +3 -3
  49. ui/static/documentation/__next._full.txt +3 -3
  50. ui/static/documentation/__next._head.txt +1 -1
  51. ui/static/documentation/__next._index.txt +2 -2
  52. ui/static/documentation/__next._tree.txt +2 -2
  53. ui/static/documentation/__next.documentation.__PAGE__.txt +2 -2
  54. ui/static/documentation/__next.documentation.txt +1 -1
  55. ui/static/documentation/index.html +2 -2
  56. ui/static/documentation/index.txt +3 -3
  57. ui/static/index.html +1 -1
  58. ui/static/index.txt +2 -2
  59. ui/static/metadata/__next._full.txt +2 -2
  60. ui/static/metadata/__next._head.txt +1 -1
  61. ui/static/metadata/__next._index.txt +2 -2
  62. ui/static/metadata/__next._tree.txt +2 -2
  63. ui/static/metadata/__next.metadata.__PAGE__.txt +1 -1
  64. ui/static/metadata/__next.metadata.txt +1 -1
  65. ui/static/metadata/index.html +1 -1
  66. ui/static/metadata/index.txt +2 -2
  67. ui/static/quality/__next._full.txt +2 -2
  68. ui/static/quality/__next._head.txt +1 -1
  69. ui/static/quality/__next._index.txt +2 -2
  70. ui/static/quality/__next._tree.txt +2 -2
  71. ui/static/quality/__next.quality.__PAGE__.txt +1 -1
  72. ui/static/quality/__next.quality.txt +1 -1
  73. ui/static/quality/index.html +2 -2
  74. ui/static/quality/index.txt +2 -2
  75. ui/static/rules/__next._full.txt +2 -2
  76. ui/static/rules/__next._head.txt +1 -1
  77. ui/static/rules/__next._index.txt +2 -2
  78. ui/static/rules/__next._tree.txt +2 -2
  79. ui/static/rules/__next.rules.__PAGE__.txt +1 -1
  80. ui/static/rules/__next.rules.txt +1 -1
  81. ui/static/rules/index.html +1 -1
  82. ui/static/rules/index.txt +2 -2
  83. ui/static/schemas/__next._full.txt +2 -2
  84. ui/static/schemas/__next._head.txt +1 -1
  85. ui/static/schemas/__next._index.txt +2 -2
  86. ui/static/schemas/__next._tree.txt +2 -2
  87. ui/static/schemas/__next.schemas.__PAGE__.txt +1 -1
  88. ui/static/schemas/__next.schemas.txt +1 -1
  89. ui/static/schemas/index.html +1 -1
  90. ui/static/schemas/index.txt +2 -2
  91. ui/static/settings/__next._full.txt +2 -2
  92. ui/static/settings/__next._head.txt +1 -1
  93. ui/static/settings/__next._index.txt +2 -2
  94. ui/static/settings/__next._tree.txt +2 -2
  95. ui/static/settings/__next.settings.__PAGE__.txt +1 -1
  96. ui/static/settings/__next.settings.txt +1 -1
  97. ui/static/settings/index.html +1 -1
  98. ui/static/settings/index.txt +2 -2
  99. ui/static/static/.gitkeep +0 -0
  100. ui/static/static/404/index.html +1 -0
  101. ui/static/static/404.html +1 -0
  102. ui/static/static/__next.__PAGE__.txt +10 -0
  103. ui/static/static/__next._full.txt +30 -0
  104. ui/static/static/__next._head.txt +7 -0
  105. ui/static/static/__next._index.txt +9 -0
  106. ui/static/static/__next._tree.txt +2 -0
  107. ui/static/static/_next/static/chunks/222442f6da32302a.js +1 -0
  108. ui/static/static/_next/static/chunks/247eb132b7f7b574.js +1 -0
  109. ui/static/static/_next/static/chunks/297d55555b71baba.js +1 -0
  110. ui/static/static/_next/static/chunks/2ab439ce003cd691.js +1 -0
  111. ui/static/static/_next/static/chunks/414e77373f8ff61c.js +1 -0
  112. ui/static/static/_next/static/chunks/49ca65abd26ae49e.js +1 -0
  113. ui/static/static/_next/static/chunks/5e04d10c4a7b58a3.js +1 -0
  114. ui/static/static/_next/static/chunks/652ad0aa26265c47.js +2 -0
  115. ui/static/static/_next/static/chunks/75d88a058d8ffaa6.js +1 -0
  116. ui/static/static/_next/static/chunks/8c89634cf6bad76f.js +1 -0
  117. ui/static/static/_next/static/chunks/9667e7a3d359eb39.js +1 -0
  118. ui/static/static/_next/static/chunks/9c23f44fff36548a.js +1 -0
  119. ui/static/static/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  120. ui/static/static/_next/static/chunks/b32a0963684b9933.js +4 -0
  121. ui/static/static/_next/static/chunks/c69f6cba366bd988.js +1 -0
  122. ui/static/static/_next/static/chunks/db913959c675cea6.js +1 -0
  123. ui/static/static/_next/static/chunks/f061a4be97bfc3b3.js +1 -0
  124. ui/static/static/_next/static/chunks/f2e7afeab1178138.js +1 -0
  125. ui/static/static/_next/static/chunks/ff1a16fafef87110.js +1 -0
  126. ui/static/static/_next/static/chunks/turbopack-ffcb7ab6794027ef.js +3 -0
  127. ui/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_buildManifest.js +11 -0
  128. ui/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_ssgManifest.js +1 -0
  129. ui/static/static/_not-found/__next._full.txt +17 -0
  130. ui/static/static/_not-found/__next._head.txt +7 -0
  131. ui/static/static/_not-found/__next._index.txt +9 -0
  132. ui/static/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
  133. ui/static/static/_not-found/__next._not-found.txt +4 -0
  134. ui/static/static/_not-found/__next._tree.txt +2 -0
  135. ui/static/static/_not-found/index.html +1 -0
  136. ui/static/static/_not-found/index.txt +17 -0
  137. ui/static/static/contracts/__next._full.txt +21 -0
  138. ui/static/static/contracts/__next._head.txt +7 -0
  139. ui/static/static/contracts/__next._index.txt +9 -0
  140. ui/static/static/contracts/__next._tree.txt +2 -0
  141. ui/static/static/contracts/__next.contracts.__PAGE__.txt +9 -0
  142. ui/static/static/contracts/__next.contracts.txt +4 -0
  143. ui/static/static/contracts/index.html +1 -0
  144. ui/static/static/contracts/index.txt +21 -0
  145. ui/static/static/documentation/__next._full.txt +21 -0
  146. ui/static/static/documentation/__next._head.txt +7 -0
  147. ui/static/static/documentation/__next._index.txt +9 -0
  148. ui/static/static/documentation/__next._tree.txt +2 -0
  149. ui/static/static/documentation/__next.documentation.__PAGE__.txt +9 -0
  150. ui/static/static/documentation/__next.documentation.txt +4 -0
  151. ui/static/static/documentation/index.html +93 -0
  152. ui/static/static/documentation/index.txt +21 -0
  153. ui/static/static/index.html +1 -0
  154. ui/static/static/index.txt +30 -0
  155. ui/static/static/metadata/__next._full.txt +21 -0
  156. ui/static/static/metadata/__next._head.txt +7 -0
  157. ui/static/static/metadata/__next._index.txt +9 -0
  158. ui/static/static/metadata/__next._tree.txt +2 -0
  159. ui/static/static/metadata/__next.metadata.__PAGE__.txt +9 -0
  160. ui/static/static/metadata/__next.metadata.txt +4 -0
  161. ui/static/static/metadata/index.html +1 -0
  162. ui/static/static/metadata/index.txt +21 -0
  163. ui/static/static/quality/__next._full.txt +21 -0
  164. ui/static/static/quality/__next._head.txt +7 -0
  165. ui/static/static/quality/__next._index.txt +9 -0
  166. ui/static/static/quality/__next._tree.txt +2 -0
  167. ui/static/static/quality/__next.quality.__PAGE__.txt +9 -0
  168. ui/static/static/quality/__next.quality.txt +4 -0
  169. ui/static/static/quality/index.html +2 -0
  170. ui/static/static/quality/index.txt +21 -0
  171. ui/static/static/rules/__next._full.txt +21 -0
  172. ui/static/static/rules/__next._head.txt +7 -0
  173. ui/static/static/rules/__next._index.txt +9 -0
  174. ui/static/static/rules/__next._tree.txt +2 -0
  175. ui/static/static/rules/__next.rules.__PAGE__.txt +9 -0
  176. ui/static/static/rules/__next.rules.txt +4 -0
  177. ui/static/static/rules/index.html +1 -0
  178. ui/static/static/rules/index.txt +21 -0
  179. ui/static/static/schemas/__next._full.txt +21 -0
  180. ui/static/static/schemas/__next._head.txt +7 -0
  181. ui/static/static/schemas/__next._index.txt +9 -0
  182. ui/static/static/schemas/__next._tree.txt +2 -0
  183. ui/static/static/schemas/__next.schemas.__PAGE__.txt +9 -0
  184. ui/static/static/schemas/__next.schemas.txt +4 -0
  185. ui/static/static/schemas/index.html +1 -0
  186. ui/static/static/schemas/index.txt +21 -0
  187. ui/static/static/settings/__next._full.txt +21 -0
  188. ui/static/static/settings/__next._head.txt +7 -0
  189. ui/static/static/settings/__next._index.txt +9 -0
  190. ui/static/static/settings/__next._tree.txt +2 -0
  191. ui/static/static/settings/__next.settings.__PAGE__.txt +9 -0
  192. ui/static/static/settings/__next.settings.txt +4 -0
  193. ui/static/static/settings/index.html +1 -0
  194. ui/static/static/settings/index.txt +21 -0
  195. ui/static/static/validation/__next._full.txt +21 -0
  196. ui/static/static/validation/__next._head.txt +7 -0
  197. ui/static/static/validation/__next._index.txt +9 -0
  198. ui/static/static/validation/__next._tree.txt +2 -0
  199. ui/static/static/validation/__next.validation.__PAGE__.txt +9 -0
  200. ui/static/static/validation/__next.validation.txt +4 -0
  201. ui/static/static/validation/index.html +1 -0
  202. ui/static/static/validation/index.txt +21 -0
  203. ui/static/validation/__next._full.txt +2 -2
  204. ui/static/validation/__next._head.txt +1 -1
  205. ui/static/validation/__next._index.txt +2 -2
  206. ui/static/validation/__next._tree.txt +2 -2
  207. ui/static/validation/__next.validation.__PAGE__.txt +1 -1
  208. ui/static/validation/__next.validation.txt +1 -1
  209. ui/static/validation/index.html +1 -1
  210. ui/static/validation/index.txt +2 -2
  211. pycharter/db/schemas/.ipynb_checkpoints/data_contract-checkpoint.py +0 -160
  212. pycharter-0.0.20.dist-info/RECORD +0 -247
  213. {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/WHEEL +0 -0
  214. {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/entry_points.txt +0 -0
  215. {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/licenses/LICENSE +0 -0
  216. {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/top_level.txt +0 -0
  217. /ui/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_buildManifest.js +0 -0
  218. /ui/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_ssgManifest.js +0 -0
  219. /ui/static/{_next → static/_next}/static/chunks/4e310fe5005770a3.css +0 -0
  220. /ui/static/{_next → static/_next}/static/chunks/5fc14c00a2779dc5.js +0 -0
  221. /ui/static/{_next → static/_next}/static/chunks/b584574fdc8ab13e.js +0 -0
  222. /ui/static/{_next → static/_next}/static/chunks/d5989c94d3614b3a.js +0 -0
api/routes/v1/metadata.py CHANGED
@@ -2,13 +2,14 @@
2
2
  Route handlers for metadata store operations.
3
3
  """
4
4
 
5
+ from typing import Any, Dict, List
6
+
5
7
  from fastapi import APIRouter, Depends, HTTPException, Query, status
6
8
  from sqlalchemy.orm import Session
7
- from typing import Any, Dict, List
8
- import uuid
9
9
 
10
10
  from api.dependencies.database import get_db_session
11
11
  from api.dependencies.store import get_metadata_store
12
+ from api.utils import ensure_uuid, get_by_id_or_404, model_to_dict, safe_uuid_to_str
12
13
  from api.models.metadata import (
13
14
  CoercionRulesStoreRequest,
14
15
  MetadataGetRequest,
@@ -599,160 +600,6 @@ async def get_complete_schema(
599
600
  )
600
601
 
601
602
 
602
- # Helper function to convert SQLAlchemy models to dictionaries
603
- def model_to_dict(model_instance) -> dict:
604
- """Convert SQLAlchemy model instance to dictionary."""
605
- if model_instance is None:
606
- return {}
607
-
608
- result = {}
609
- for column in model_instance.__table__.columns:
610
- value = getattr(model_instance, column.name, None)
611
- if value is None:
612
- result[column.name] = None
613
- # Convert UUID to string
614
- elif hasattr(value, 'hex'):
615
- result[column.name] = str(value)
616
- # Convert datetime to ISO format string
617
- elif hasattr(value, 'isoformat'):
618
- result[column.name] = value.isoformat()
619
- # Keep JSON fields as-is
620
- elif isinstance(value, (dict, list)):
621
- result[column.name] = value
622
- else:
623
- result[column.name] = value
624
- return result
625
-
626
-
627
- @router.get(
628
- "/metadata/owners",
629
- status_code=status.HTTP_200_OK,
630
- summary="List all owners",
631
- description="Retrieve a list of all owners from the database",
632
- response_description="List of owners",
633
- )
634
- async def list_owners(
635
- db: Session = Depends(get_db_session),
636
- ) -> list[dict]:
637
- """
638
- List all owners in the database.
639
-
640
- Returns:
641
- List of owner dictionaries
642
- """
643
- owners = db.query(OwnerModel).all()
644
- return [model_to_dict(owner) for owner in owners]
645
-
646
-
647
- @router.get(
648
- "/metadata/domains",
649
- status_code=status.HTTP_200_OK,
650
- summary="List all domains",
651
- description="Retrieve a list of all domains from the database",
652
- response_description="List of domains",
653
- )
654
- async def list_domains(
655
- db: Session = Depends(get_db_session),
656
- ) -> list[dict]:
657
- """
658
- List all domains in the database.
659
-
660
- Returns:
661
- List of domain dictionaries
662
- """
663
- domains = db.query(DomainModel).all()
664
- return [model_to_dict(domain) for domain in domains]
665
-
666
-
667
- @router.get(
668
- "/metadata/systems",
669
- status_code=status.HTTP_200_OK,
670
- summary="List all systems",
671
- description="Retrieve a list of all systems from the database",
672
- response_description="List of systems",
673
- )
674
- async def list_systems(
675
- db: Session = Depends(get_db_session),
676
- ) -> list[dict]:
677
- """
678
- List all systems in the database.
679
-
680
- Returns:
681
- List of system dictionaries
682
- """
683
- systems = db.query(SystemModel).all()
684
- return [model_to_dict(system) for system in systems]
685
-
686
-
687
- @router.get(
688
- "/metadata/environments",
689
- status_code=status.HTTP_200_OK,
690
- summary="List all environments",
691
- description="Retrieve a list of all environments from the database",
692
- response_description="List of environments",
693
- )
694
- async def list_environments(
695
- db: Session = Depends(get_db_session),
696
- ) -> list[dict]:
697
- """
698
- List all environments in the database.
699
-
700
- Returns:
701
- List of environment dictionaries
702
- """
703
- environments = db.query(EnvironmentModel).all()
704
- return [model_to_dict(env) for env in environments]
705
-
706
-
707
- @router.get(
708
- "/metadata/storage_locations",
709
- status_code=status.HTTP_200_OK,
710
- summary="List all storage locations",
711
- description="Retrieve a list of all storage locations from the database",
712
- response_description="List of storage locations",
713
- )
714
- async def list_storage_locations(
715
- db: Session = Depends(get_db_session),
716
- ) -> list[dict]:
717
- """
718
- List all storage locations in the database.
719
-
720
- Returns:
721
- List of storage location dictionaries
722
- """
723
- storage_locations = db.query(StorageLocationModel).all()
724
- result = []
725
- for sl in storage_locations:
726
- data = model_to_dict(sl)
727
- # Include related system and environment names if available
728
- if sl.system:
729
- data['system_name'] = sl.system.name
730
- if sl.environment:
731
- data['environment_name'] = sl.environment.name
732
- result.append(data)
733
- return result
734
-
735
-
736
- @router.get(
737
- "/metadata/tags",
738
- status_code=status.HTTP_200_OK,
739
- summary="List all tags",
740
- description="Retrieve a list of all tags from the database",
741
- response_description="List of tags",
742
- )
743
- async def list_tags(
744
- db: Session = Depends(get_db_session),
745
- ) -> list[dict]:
746
- """
747
- List all tags in the database.
748
-
749
- Returns:
750
- List of tag dictionaries
751
- """
752
- tags = db.query(TagModel).all()
753
- return [model_to_dict(tag) for tag in tags]
754
-
755
-
756
603
  @router.get(
757
604
  "/metadata/{entity_type}",
758
605
  status_code=status.HTTP_200_OK,
@@ -763,7 +610,7 @@ async def list_tags(
763
610
  async def list_metadata_entities(
764
611
  entity_type: str,
765
612
  db: Session = Depends(get_db_session),
766
- ) -> list[dict]:
613
+ ) -> List[Dict[str, Any]]:
767
614
  """
768
615
  List metadata entities by type.
769
616
 
@@ -818,7 +665,7 @@ async def list_metadata_entities(
818
665
  return [model_to_dict(entity) for entity in entities]
819
666
 
820
667
 
821
- # Create endpoints
668
+ # Create endpoints (POST routes for creating entities)
822
669
  @router.post(
823
670
  "/metadata/owners",
824
671
  status_code=status.HTTP_201_CREATED,
@@ -829,7 +676,7 @@ async def list_metadata_entities(
829
676
  async def create_owner(
830
677
  request: OwnerCreateRequest,
831
678
  db: Session = Depends(get_db_session),
832
- ) -> dict:
679
+ ) -> Dict[str, Any]:
833
680
  """Create a new owner."""
834
681
  # Check if owner already exists
835
682
  existing = db.query(OwnerModel).filter(OwnerModel.id == request.id).first()
@@ -862,7 +709,7 @@ async def create_owner(
862
709
  async def create_domain(
863
710
  request: DomainCreateRequest,
864
711
  db: Session = Depends(get_db_session),
865
- ) -> dict:
712
+ ) -> Dict[str, Any]:
866
713
  """Create a new domain."""
867
714
  # Check if domain already exists
868
715
  existing = db.query(DomainModel).filter(DomainModel.name == request.name).first()
@@ -892,7 +739,7 @@ async def create_domain(
892
739
  async def create_system(
893
740
  request: SystemCreateRequest,
894
741
  db: Session = Depends(get_db_session),
895
- ) -> dict:
742
+ ) -> Dict[str, Any]:
896
743
  """Create a new system."""
897
744
  # Check if system already exists
898
745
  existing = db.query(SystemModel).filter(SystemModel.name == request.name).first()
@@ -923,7 +770,7 @@ async def create_system(
923
770
  async def create_environment(
924
771
  request: EnvironmentCreateRequest,
925
772
  db: Session = Depends(get_db_session),
926
- ) -> dict:
773
+ ) -> Dict[str, Any]:
927
774
  """Create a new environment."""
928
775
  # Check if environment already exists
929
776
  existing = db.query(EnvironmentModel).filter(EnvironmentModel.name == request.name).first()
@@ -956,7 +803,7 @@ async def create_environment(
956
803
  async def create_storage_location(
957
804
  request: StorageLocationCreateRequest,
958
805
  db: Session = Depends(get_db_session),
959
- ) -> dict:
806
+ ) -> Dict[str, Any]:
960
807
  """Create a new storage location."""
961
808
  # Check if storage location already exists
962
809
  existing = db.query(StorageLocationModel).filter(StorageLocationModel.name == request.name).first()
@@ -1002,7 +849,7 @@ async def create_storage_location(
1002
849
  async def create_tag(
1003
850
  request: TagCreateRequest,
1004
851
  db: Session = Depends(get_db_session),
1005
- ) -> dict:
852
+ ) -> Dict[str, Any]:
1006
853
  """Create a new tag."""
1007
854
  # Check if tag already exists
1008
855
  existing = db.query(TagModel).filter(TagModel.name == request.name).first()
@@ -1037,14 +884,13 @@ async def update_owner(
1037
884
  owner_id: str,
1038
885
  request: OwnerUpdateRequest,
1039
886
  db: Session = Depends(get_db_session),
1040
- ) -> dict:
887
+ ) -> Dict[str, Any]:
1041
888
  """Update an existing owner."""
1042
- owner = db.query(OwnerModel).filter(OwnerModel.id == owner_id).first()
1043
- if not owner:
1044
- raise HTTPException(
1045
- status_code=status.HTTP_404_NOT_FOUND,
1046
- detail=f"Owner with ID '{owner_id}' not found",
1047
- )
889
+ owner = get_by_id_or_404(
890
+ db, OwnerModel, owner_id,
891
+ error_message=f"Owner with ID '{owner_id}' not found",
892
+ model_name="Owner"
893
+ )
1048
894
 
1049
895
  if request.name is not None:
1050
896
  owner.name = request.name
@@ -1071,22 +917,21 @@ async def update_domain(
1071
917
  domain_id: str,
1072
918
  request: DomainUpdateRequest,
1073
919
  db: Session = Depends(get_db_session),
1074
- ) -> dict:
920
+ ) -> Dict[str, Any]:
1075
921
  """Update an existing domain."""
1076
922
  try:
1077
- domain_uuid = uuid.UUID(domain_id)
923
+ ensure_uuid(domain_id)
1078
924
  except ValueError:
1079
925
  raise HTTPException(
1080
926
  status_code=status.HTTP_400_BAD_REQUEST,
1081
927
  detail=f"Invalid domain ID format: {domain_id}",
1082
928
  )
1083
929
 
1084
- domain = db.query(DomainModel).filter(DomainModel.id == domain_uuid).first()
1085
- if not domain:
1086
- raise HTTPException(
1087
- status_code=status.HTTP_404_NOT_FOUND,
1088
- detail=f"Domain with ID '{domain_id}' not found",
1089
- )
930
+ domain = get_by_id_or_404(
931
+ db, DomainModel, domain_id,
932
+ error_message=f"Domain with ID '{domain_id}' not found",
933
+ model_name="Domain"
934
+ )
1090
935
 
1091
936
  if request.name is not None:
1092
937
  domain.name = request.name
@@ -1109,22 +954,21 @@ async def update_system(
1109
954
  system_id: str,
1110
955
  request: SystemUpdateRequest,
1111
956
  db: Session = Depends(get_db_session),
1112
- ) -> dict:
957
+ ) -> Dict[str, Any]:
1113
958
  """Update an existing system."""
1114
959
  try:
1115
- system_uuid = uuid.UUID(system_id)
960
+ ensure_uuid(system_id)
1116
961
  except ValueError:
1117
962
  raise HTTPException(
1118
963
  status_code=status.HTTP_400_BAD_REQUEST,
1119
964
  detail=f"Invalid system ID format: {system_id}",
1120
965
  )
1121
966
 
1122
- system = db.query(SystemModel).filter(SystemModel.id == system_uuid).first()
1123
- if not system:
1124
- raise HTTPException(
1125
- status_code=status.HTTP_404_NOT_FOUND,
1126
- detail=f"System with ID '{system_id}' not found",
1127
- )
967
+ system = get_by_id_or_404(
968
+ db, SystemModel, system_id,
969
+ error_message=f"System with ID '{system_id}' not found",
970
+ model_name="System"
971
+ )
1128
972
 
1129
973
  if request.name is not None:
1130
974
  system.name = request.name
@@ -1149,22 +993,21 @@ async def update_environment(
1149
993
  environment_id: str,
1150
994
  request: EnvironmentUpdateRequest,
1151
995
  db: Session = Depends(get_db_session),
1152
- ) -> dict:
996
+ ) -> Dict[str, Any]:
1153
997
  """Update an existing environment."""
1154
998
  try:
1155
- env_uuid = uuid.UUID(environment_id)
999
+ ensure_uuid(environment_id)
1156
1000
  except ValueError:
1157
1001
  raise HTTPException(
1158
1002
  status_code=status.HTTP_400_BAD_REQUEST,
1159
1003
  detail=f"Invalid environment ID format: {environment_id}",
1160
1004
  )
1161
1005
 
1162
- environment = db.query(EnvironmentModel).filter(EnvironmentModel.id == env_uuid).first()
1163
- if not environment:
1164
- raise HTTPException(
1165
- status_code=status.HTTP_404_NOT_FOUND,
1166
- detail=f"Environment with ID '{environment_id}' not found",
1167
- )
1006
+ environment = get_by_id_or_404(
1007
+ db, EnvironmentModel, environment_id,
1008
+ error_message=f"Environment with ID '{environment_id}' not found",
1009
+ model_name="Environment"
1010
+ )
1168
1011
 
1169
1012
  if request.name is not None:
1170
1013
  environment.name = request.name
@@ -1193,22 +1036,21 @@ async def update_storage_location(
1193
1036
  storage_location_id: str,
1194
1037
  request: StorageLocationUpdateRequest,
1195
1038
  db: Session = Depends(get_db_session),
1196
- ) -> dict:
1039
+ ) -> Dict[str, Any]:
1197
1040
  """Update an existing storage location."""
1198
1041
  try:
1199
- sl_uuid = uuid.UUID(storage_location_id)
1042
+ ensure_uuid(storage_location_id)
1200
1043
  except ValueError:
1201
1044
  raise HTTPException(
1202
1045
  status_code=status.HTTP_400_BAD_REQUEST,
1203
1046
  detail=f"Invalid storage location ID format: {storage_location_id}",
1204
1047
  )
1205
1048
 
1206
- storage_location = db.query(StorageLocationModel).filter(StorageLocationModel.id == sl_uuid).first()
1207
- if not storage_location:
1208
- raise HTTPException(
1209
- status_code=status.HTTP_404_NOT_FOUND,
1210
- detail=f"Storage location with ID '{storage_location_id}' not found",
1211
- )
1049
+ storage_location = get_by_id_or_404(
1050
+ db, StorageLocationModel, storage_location_id,
1051
+ error_message=f"Storage location with ID '{storage_location_id}' not found",
1052
+ model_name="StorageLocation"
1053
+ )
1212
1054
 
1213
1055
  if request.name is not None:
1214
1056
  storage_location.name = request.name
@@ -1256,22 +1098,21 @@ async def update_tag(
1256
1098
  tag_id: str,
1257
1099
  request: TagUpdateRequest,
1258
1100
  db: Session = Depends(get_db_session),
1259
- ) -> dict:
1101
+ ) -> Dict[str, Any]:
1260
1102
  """Update an existing tag."""
1261
1103
  try:
1262
- tag_uuid = uuid.UUID(tag_id)
1104
+ ensure_uuid(tag_id)
1263
1105
  except ValueError:
1264
1106
  raise HTTPException(
1265
1107
  status_code=status.HTTP_400_BAD_REQUEST,
1266
1108
  detail=f"Invalid tag ID format: {tag_id}",
1267
1109
  )
1268
1110
 
1269
- tag = db.query(TagModel).filter(TagModel.id == tag_uuid).first()
1270
- if not tag:
1271
- raise HTTPException(
1272
- status_code=status.HTTP_404_NOT_FOUND,
1273
- detail=f"Tag with ID '{tag_id}' not found",
1274
- )
1111
+ tag = get_by_id_or_404(
1112
+ db, TagModel, tag_id,
1113
+ error_message=f"Tag with ID '{tag_id}' not found",
1114
+ model_name="Tag"
1115
+ )
1275
1116
 
1276
1117
  if request.name is not None:
1277
1118
  tag.name = request.name
api/routes/v1/schemas.py CHANGED
@@ -47,7 +47,7 @@ async def generate_schema(
47
47
 
48
48
  return SchemaGenerateResponse(
49
49
  model_name=request.model_name,
50
- schema_json=schema_json,
50
+ schema_definition=schema_json, # Field alias maps to "schema_json" in JSON
51
51
  message=f"Model '{request.model_name}' generated successfully",
52
52
  )
53
53
 
api/routes/v1/settings.py CHANGED
@@ -2,16 +2,32 @@
2
2
  Route handlers for settings and configuration testing.
3
3
  """
4
4
 
5
+ import os
6
+ from pathlib import Path
5
7
  from typing import Optional
6
- from fastapi import APIRouter, HTTPException, status
8
+ from fastapi import APIRouter, Depends, HTTPException, status
7
9
  from pydantic import BaseModel
8
10
  import sqlalchemy
9
11
  from sqlalchemy import create_engine, text
10
12
  from sqlalchemy.exc import SQLAlchemyError
13
+ from sqlalchemy.orm import Session
14
+
15
+ from pycharter.config import get_database_url
16
+ from api.dependencies.database import get_db_session
11
17
 
12
18
  router = APIRouter()
13
19
 
14
20
 
21
+ class DatabaseConfigResponse(BaseModel):
22
+ """Response model for database configuration info."""
23
+ configured_url: Optional[str]
24
+ actual_url: str
25
+ database_type: str
26
+ is_default: bool
27
+ contract_count: int
28
+ message: str
29
+
30
+
15
31
  class DatabaseTestRequest(BaseModel):
16
32
  connection_string: Optional[str] = None
17
33
  host: Optional[str] = None
@@ -58,6 +74,77 @@ class DlqStatsResponse(BaseModel):
58
74
  by_status: dict[str, int]
59
75
 
60
76
 
77
+ @router.get(
78
+ "/settings/database-config",
79
+ response_model=DatabaseConfigResponse,
80
+ status_code=status.HTTP_200_OK,
81
+ summary="Get current database configuration",
82
+ description="Get information about the currently configured database connection",
83
+ )
84
+ async def get_database_config(
85
+ db: Session = Depends(get_db_session),
86
+ ) -> DatabaseConfigResponse:
87
+ """
88
+ Get current database configuration and connection info.
89
+
90
+ Shows which database is actually being used, which helps diagnose
91
+ issues where SQLite might be used instead of PostgreSQL.
92
+ """
93
+ from pycharter.db.models import DataContractModel
94
+
95
+ configured_url = get_database_url()
96
+
97
+ # Get actual URL from the session
98
+ actual_url = str(db.bind.url) if hasattr(db, 'bind') and db.bind else "unknown"
99
+
100
+ # Determine database type
101
+ if actual_url.startswith("sqlite"):
102
+ database_type = "SQLite"
103
+ is_default = configured_url is None
104
+ elif actual_url.startswith(("postgresql", "postgres")):
105
+ database_type = "PostgreSQL"
106
+ is_default = False
107
+ else:
108
+ database_type = "Unknown"
109
+ is_default = False
110
+
111
+ # Count contracts in current database
112
+ try:
113
+ contract_count = db.query(DataContractModel).count()
114
+ except Exception:
115
+ contract_count = -1
116
+
117
+ # Build message
118
+ if is_default:
119
+ message = (
120
+ f"⚠️ No database URL configured. Using default SQLite database.\n"
121
+ f"To use PostgreSQL, set the PYCHARTER_DATABASE_URL environment variable:\n"
122
+ f" export PYCHARTER_DATABASE_URL='postgresql://user:password@localhost:5432/pycharter'"
123
+ )
124
+ else:
125
+ # Mask password in configured URL
126
+ masked_configured = configured_url
127
+ if configured_url and "@" in configured_url:
128
+ parts = configured_url.split("@", 1)
129
+ if ":" in parts[0] and "://" in parts[0]:
130
+ scheme_user = parts[0].split("://", 1)[0]
131
+ user_pass = parts[0].split("://", 1)[1]
132
+ if ":" in user_pass:
133
+ user, _ = user_pass.split(":", 1)
134
+ masked_configured = f"{scheme_user}://{user}:****@{parts[1]}"
135
+
136
+ message = f"✓ Using {database_type} database"
137
+
138
+ return DatabaseConfigResponse(
139
+ configured_url=masked_configured if 'masked_configured' in locals() else configured_url,
140
+ actual_url=actual_url.split("@")[-1] if "@" in actual_url else actual_url, # Show only host/db part
141
+ database_type=database_type,
142
+ is_default=is_default,
143
+ contract_count=contract_count,
144
+ message=message,
145
+ )
146
+
147
+
61
148
  def _build_connection_string(
62
149
  connection_string: Optional[str] = None,
63
150
  host: Optional[str] = None,