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.
- api/dependencies/__init__.py +2 -1
- api/dependencies/database.py +71 -5
- api/main.py +47 -8
- api/models/contracts.py +6 -4
- api/models/metadata.py +11 -7
- api/models/schemas.py +16 -10
- api/routes/v1/contracts.py +498 -226
- api/routes/v1/metadata.py +52 -211
- api/routes/v1/schemas.py +1 -1
- api/routes/v1/settings.py +88 -1
- api/utils.py +224 -0
- pycharter/__init__.py +149 -93
- pycharter/data/templates/template_transform_advanced.yaml +50 -0
- pycharter/data/templates/template_transform_simple.yaml +59 -0
- pycharter/db/models/base.py +1 -2
- pycharter/etl_generator/orchestrator.py +463 -487
- pycharter/metadata_store/postgres.py +16 -191
- pycharter/metadata_store/sqlite.py +12 -41
- {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/METADATA +284 -62
- pycharter-0.0.22.dist-info/RECORD +358 -0
- ui/static/404/index.html +1 -1
- ui/static/404.html +1 -1
- ui/static/__next.__PAGE__.txt +1 -1
- ui/static/__next._full.txt +2 -2
- ui/static/__next._head.txt +1 -1
- ui/static/__next._index.txt +2 -2
- ui/static/__next._tree.txt +2 -2
- ui/static/_next/static/chunks/13d4a0fbd74c1ee4.js +1 -0
- ui/static/_next/static/chunks/2edb43b48432ac04.js +441 -0
- ui/static/_next/static/chunks/c4fa4f4114b7c352.js +1 -0
- ui/static/_next/static/chunks/d2363397e1b2bcab.css +1 -0
- ui/static/_next/static/chunks/f7d1a90dd75d2572.js +1 -0
- ui/static/_not-found/__next._full.txt +2 -2
- ui/static/_not-found/__next._head.txt +1 -1
- ui/static/_not-found/__next._index.txt +2 -2
- ui/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
- ui/static/_not-found/__next._not-found.txt +1 -1
- ui/static/_not-found/__next._tree.txt +2 -2
- ui/static/_not-found/index.html +1 -1
- ui/static/_not-found/index.txt +2 -2
- ui/static/contracts/__next._full.txt +3 -3
- ui/static/contracts/__next._head.txt +1 -1
- ui/static/contracts/__next._index.txt +2 -2
- ui/static/contracts/__next._tree.txt +2 -2
- ui/static/contracts/__next.contracts.__PAGE__.txt +2 -2
- ui/static/contracts/__next.contracts.txt +1 -1
- ui/static/contracts/index.html +1 -1
- ui/static/contracts/index.txt +3 -3
- ui/static/documentation/__next._full.txt +3 -3
- ui/static/documentation/__next._head.txt +1 -1
- ui/static/documentation/__next._index.txt +2 -2
- ui/static/documentation/__next._tree.txt +2 -2
- ui/static/documentation/__next.documentation.__PAGE__.txt +2 -2
- ui/static/documentation/__next.documentation.txt +1 -1
- ui/static/documentation/index.html +2 -2
- ui/static/documentation/index.txt +3 -3
- ui/static/index.html +1 -1
- ui/static/index.txt +2 -2
- ui/static/metadata/__next._full.txt +2 -2
- ui/static/metadata/__next._head.txt +1 -1
- ui/static/metadata/__next._index.txt +2 -2
- ui/static/metadata/__next._tree.txt +2 -2
- ui/static/metadata/__next.metadata.__PAGE__.txt +1 -1
- ui/static/metadata/__next.metadata.txt +1 -1
- ui/static/metadata/index.html +1 -1
- ui/static/metadata/index.txt +2 -2
- ui/static/quality/__next._full.txt +2 -2
- ui/static/quality/__next._head.txt +1 -1
- ui/static/quality/__next._index.txt +2 -2
- ui/static/quality/__next._tree.txt +2 -2
- ui/static/quality/__next.quality.__PAGE__.txt +1 -1
- ui/static/quality/__next.quality.txt +1 -1
- ui/static/quality/index.html +2 -2
- ui/static/quality/index.txt +2 -2
- ui/static/rules/__next._full.txt +2 -2
- ui/static/rules/__next._head.txt +1 -1
- ui/static/rules/__next._index.txt +2 -2
- ui/static/rules/__next._tree.txt +2 -2
- ui/static/rules/__next.rules.__PAGE__.txt +1 -1
- ui/static/rules/__next.rules.txt +1 -1
- ui/static/rules/index.html +1 -1
- ui/static/rules/index.txt +2 -2
- ui/static/schemas/__next._full.txt +2 -2
- ui/static/schemas/__next._head.txt +1 -1
- ui/static/schemas/__next._index.txt +2 -2
- ui/static/schemas/__next._tree.txt +2 -2
- ui/static/schemas/__next.schemas.__PAGE__.txt +1 -1
- ui/static/schemas/__next.schemas.txt +1 -1
- ui/static/schemas/index.html +1 -1
- ui/static/schemas/index.txt +2 -2
- ui/static/settings/__next._full.txt +2 -2
- ui/static/settings/__next._head.txt +1 -1
- ui/static/settings/__next._index.txt +2 -2
- ui/static/settings/__next._tree.txt +2 -2
- ui/static/settings/__next.settings.__PAGE__.txt +1 -1
- ui/static/settings/__next.settings.txt +1 -1
- ui/static/settings/index.html +1 -1
- ui/static/settings/index.txt +2 -2
- ui/static/static/.gitkeep +0 -0
- ui/static/static/404/index.html +1 -0
- ui/static/static/404.html +1 -0
- ui/static/static/__next.__PAGE__.txt +10 -0
- ui/static/static/__next._full.txt +30 -0
- ui/static/static/__next._head.txt +7 -0
- ui/static/static/__next._index.txt +9 -0
- ui/static/static/__next._tree.txt +2 -0
- ui/static/static/_next/static/chunks/222442f6da32302a.js +1 -0
- ui/static/static/_next/static/chunks/247eb132b7f7b574.js +1 -0
- ui/static/static/_next/static/chunks/297d55555b71baba.js +1 -0
- ui/static/static/_next/static/chunks/2ab439ce003cd691.js +1 -0
- ui/static/static/_next/static/chunks/414e77373f8ff61c.js +1 -0
- ui/static/static/_next/static/chunks/49ca65abd26ae49e.js +1 -0
- ui/static/static/_next/static/chunks/5e04d10c4a7b58a3.js +1 -0
- ui/static/static/_next/static/chunks/652ad0aa26265c47.js +2 -0
- ui/static/static/_next/static/chunks/75d88a058d8ffaa6.js +1 -0
- ui/static/static/_next/static/chunks/8c89634cf6bad76f.js +1 -0
- ui/static/static/_next/static/chunks/9667e7a3d359eb39.js +1 -0
- ui/static/static/_next/static/chunks/9c23f44fff36548a.js +1 -0
- ui/static/static/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- ui/static/static/_next/static/chunks/b32a0963684b9933.js +4 -0
- ui/static/static/_next/static/chunks/c69f6cba366bd988.js +1 -0
- ui/static/static/_next/static/chunks/db913959c675cea6.js +1 -0
- ui/static/static/_next/static/chunks/f061a4be97bfc3b3.js +1 -0
- ui/static/static/_next/static/chunks/f2e7afeab1178138.js +1 -0
- ui/static/static/_next/static/chunks/ff1a16fafef87110.js +1 -0
- ui/static/static/_next/static/chunks/turbopack-ffcb7ab6794027ef.js +3 -0
- ui/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_buildManifest.js +11 -0
- ui/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_ssgManifest.js +1 -0
- ui/static/static/_not-found/__next._full.txt +17 -0
- ui/static/static/_not-found/__next._head.txt +7 -0
- ui/static/static/_not-found/__next._index.txt +9 -0
- ui/static/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
- ui/static/static/_not-found/__next._not-found.txt +4 -0
- ui/static/static/_not-found/__next._tree.txt +2 -0
- ui/static/static/_not-found/index.html +1 -0
- ui/static/static/_not-found/index.txt +17 -0
- ui/static/static/contracts/__next._full.txt +21 -0
- ui/static/static/contracts/__next._head.txt +7 -0
- ui/static/static/contracts/__next._index.txt +9 -0
- ui/static/static/contracts/__next._tree.txt +2 -0
- ui/static/static/contracts/__next.contracts.__PAGE__.txt +9 -0
- ui/static/static/contracts/__next.contracts.txt +4 -0
- ui/static/static/contracts/index.html +1 -0
- ui/static/static/contracts/index.txt +21 -0
- ui/static/static/documentation/__next._full.txt +21 -0
- ui/static/static/documentation/__next._head.txt +7 -0
- ui/static/static/documentation/__next._index.txt +9 -0
- ui/static/static/documentation/__next._tree.txt +2 -0
- ui/static/static/documentation/__next.documentation.__PAGE__.txt +9 -0
- ui/static/static/documentation/__next.documentation.txt +4 -0
- ui/static/static/documentation/index.html +93 -0
- ui/static/static/documentation/index.txt +21 -0
- ui/static/static/index.html +1 -0
- ui/static/static/index.txt +30 -0
- ui/static/static/metadata/__next._full.txt +21 -0
- ui/static/static/metadata/__next._head.txt +7 -0
- ui/static/static/metadata/__next._index.txt +9 -0
- ui/static/static/metadata/__next._tree.txt +2 -0
- ui/static/static/metadata/__next.metadata.__PAGE__.txt +9 -0
- ui/static/static/metadata/__next.metadata.txt +4 -0
- ui/static/static/metadata/index.html +1 -0
- ui/static/static/metadata/index.txt +21 -0
- ui/static/static/quality/__next._full.txt +21 -0
- ui/static/static/quality/__next._head.txt +7 -0
- ui/static/static/quality/__next._index.txt +9 -0
- ui/static/static/quality/__next._tree.txt +2 -0
- ui/static/static/quality/__next.quality.__PAGE__.txt +9 -0
- ui/static/static/quality/__next.quality.txt +4 -0
- ui/static/static/quality/index.html +2 -0
- ui/static/static/quality/index.txt +21 -0
- ui/static/static/rules/__next._full.txt +21 -0
- ui/static/static/rules/__next._head.txt +7 -0
- ui/static/static/rules/__next._index.txt +9 -0
- ui/static/static/rules/__next._tree.txt +2 -0
- ui/static/static/rules/__next.rules.__PAGE__.txt +9 -0
- ui/static/static/rules/__next.rules.txt +4 -0
- ui/static/static/rules/index.html +1 -0
- ui/static/static/rules/index.txt +21 -0
- ui/static/static/schemas/__next._full.txt +21 -0
- ui/static/static/schemas/__next._head.txt +7 -0
- ui/static/static/schemas/__next._index.txt +9 -0
- ui/static/static/schemas/__next._tree.txt +2 -0
- ui/static/static/schemas/__next.schemas.__PAGE__.txt +9 -0
- ui/static/static/schemas/__next.schemas.txt +4 -0
- ui/static/static/schemas/index.html +1 -0
- ui/static/static/schemas/index.txt +21 -0
- ui/static/static/settings/__next._full.txt +21 -0
- ui/static/static/settings/__next._head.txt +7 -0
- ui/static/static/settings/__next._index.txt +9 -0
- ui/static/static/settings/__next._tree.txt +2 -0
- ui/static/static/settings/__next.settings.__PAGE__.txt +9 -0
- ui/static/static/settings/__next.settings.txt +4 -0
- ui/static/static/settings/index.html +1 -0
- ui/static/static/settings/index.txt +21 -0
- ui/static/static/validation/__next._full.txt +21 -0
- ui/static/static/validation/__next._head.txt +7 -0
- ui/static/static/validation/__next._index.txt +9 -0
- ui/static/static/validation/__next._tree.txt +2 -0
- ui/static/static/validation/__next.validation.__PAGE__.txt +9 -0
- ui/static/static/validation/__next.validation.txt +4 -0
- ui/static/static/validation/index.html +1 -0
- ui/static/static/validation/index.txt +21 -0
- ui/static/validation/__next._full.txt +2 -2
- ui/static/validation/__next._head.txt +1 -1
- ui/static/validation/__next._index.txt +2 -2
- ui/static/validation/__next._tree.txt +2 -2
- ui/static/validation/__next.validation.__PAGE__.txt +1 -1
- ui/static/validation/__next.validation.txt +1 -1
- ui/static/validation/index.html +1 -1
- ui/static/validation/index.txt +2 -2
- pycharter/db/schemas/.ipynb_checkpoints/data_contract-checkpoint.py +0 -160
- pycharter-0.0.20.dist-info/RECORD +0 -247
- {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/WHEEL +0 -0
- {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/entry_points.txt +0 -0
- {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/licenses/LICENSE +0 -0
- {pycharter-0.0.20.dist-info → pycharter-0.0.22.dist-info}/top_level.txt +0 -0
- /ui/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_buildManifest.js +0 -0
- /ui/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_ssgManifest.js +0 -0
- /ui/static/{_next → static/_next}/static/chunks/4e310fe5005770a3.css +0 -0
- /ui/static/{_next → static/_next}/static/chunks/5fc14c00a2779dc5.js +0 -0
- /ui/static/{_next → static/_next}/static/chunks/b584574fdc8ab13e.js +0 -0
- /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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
887
|
+
) -> Dict[str, Any]:
|
|
1041
888
|
"""Update an existing owner."""
|
|
1042
|
-
owner =
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
) ->
|
|
920
|
+
) -> Dict[str, Any]:
|
|
1075
921
|
"""Update an existing domain."""
|
|
1076
922
|
try:
|
|
1077
|
-
|
|
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 =
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
) ->
|
|
957
|
+
) -> Dict[str, Any]:
|
|
1113
958
|
"""Update an existing system."""
|
|
1114
959
|
try:
|
|
1115
|
-
|
|
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 =
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
) ->
|
|
996
|
+
) -> Dict[str, Any]:
|
|
1153
997
|
"""Update an existing environment."""
|
|
1154
998
|
try:
|
|
1155
|
-
|
|
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 =
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
) ->
|
|
1039
|
+
) -> Dict[str, Any]:
|
|
1197
1040
|
"""Update an existing storage location."""
|
|
1198
1041
|
try:
|
|
1199
|
-
|
|
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 =
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
) ->
|
|
1101
|
+
) -> Dict[str, Any]:
|
|
1260
1102
|
"""Update an existing tag."""
|
|
1261
1103
|
try:
|
|
1262
|
-
|
|
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 =
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
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,
|