Skip to content

Commit cf9e760

Browse files
committed
Adjust namespace to use name as unique ID
1 parent e79fadb commit cf9e760

File tree

3 files changed

+86
-88
lines changed

3 files changed

+86
-88
lines changed

src/api/api.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from pydantic import BaseModel
1+
from pydantic import BaseModel, Field, field_validator
22
from typing import List
33
from datetime import datetime
4+
import re
45

56

67
class AudioServiceSteps:
@@ -56,7 +57,6 @@ class VoiceCloneProgress(Progress):
5657
class Namespace(BaseModel):
5758
"""Namespace model."""
5859

59-
namespaceID: str
6060
name: str
6161
createdAt: datetime
6262
homePath: str
@@ -66,13 +66,29 @@ class Namespace(BaseModel):
6666
CreateNamespaceResponse = Namespace
6767

6868
class CreateNamespaceRequest(BaseModel):
69-
name: str
69+
name: str = Field(..., min_length=1, max_length=64)
70+
71+
@field_validator("name")
72+
@classmethod
73+
def validate_name(cls, name: str):
74+
# Ensure it doesn't contain invalid characters
75+
if "/" in name or "\0" in name:
76+
raise ValueError("Namespace name cannot contain '/' or null characters")
77+
78+
# Optional: Avoid names with only dots ('.' or '..')
79+
if name in {".", ".."}:
80+
raise ValueError("Namespace name cannot be '.' or '..'")
81+
82+
# Optional: Ensure it consists of valid filename characters
83+
if not re.match(r"^[\w.-]+$", name): # Allows letters, numbers, underscores, dashes, and dots
84+
raise ValueError("Namespace name contains invalid characters")
85+
86+
return name
7087

7188

7289
class UpdateNamespaceRequest(BaseModel):
7390
"""Request model for updating a namespace."""
7491

75-
namespaceID: str
7692
name: str
7793

7894

src/rest/rest.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ def _register_routes(self):
5151
summary="Create a new namespace",
5252
)
5353
self.router.add_api_route(
54-
path="/namespaces/{namespace_id}",
54+
path="/namespaces/{name}",
5555
endpoint=self.change_namespace,
5656
methods=["PUT"],
5757
response_model=Namespace,
5858
summary="Update a namespace",
5959
)
6060
self.router.add_api_route(
61-
path="/namespaces/{namespace_id}",
61+
path="/namespaces/{name}",
6262
endpoint=self.remove_namespace,
6363
methods=["DELETE"],
6464
status_code=204, # No body in response
@@ -72,25 +72,29 @@ async def list_namespaces(self):
7272

7373
async def new_namespace(self, create_request: CreateNamespaceRequest):
7474
"""Create a new namespace."""
75-
namespace = self.namespace_service.create_namespace(create_request.name)
76-
return namespace
75+
try:
76+
return self.namespace_service.create_namespace(create_request.name)
77+
except FileExistsError:
78+
raise HTTPException(status_code=409, detail="Namespace already exists")
79+
except ValueError as e:
80+
raise HTTPException(status_code=400, detail=str(e))
7781

78-
async def change_namespace(self, namespace_id: str, update_request: UpdateNamespaceRequest):
82+
async def change_namespace(self, name: str, update_request: UpdateNamespaceRequest):
7983
"""Update a namespace."""
8084
try:
81-
namespace = self.namespace_service.update_namespace(namespace_id, update_request.name)
82-
return namespace
85+
return self.namespace_service.update_namespace(name, update_request.name)
86+
except FileExistsError:
87+
raise HTTPException(status_code=409, detail="Namespace already exists")
8388
except ValueError as e:
8489
raise HTTPException(status_code=404, detail=str(e))
8590

86-
async def remove_namespace(self, namespace_id: str):
91+
async def remove_namespace(self, name: str):
8792
"""Delete a namespace."""
8893
try:
89-
self.namespace_service.delete_namespace(namespace_id)
94+
self.namespace_service.delete_namespace(name)
9095
except ValueError as e:
9196
raise HTTPException(status_code=404, detail=str(e))
9297

93-
9498
class FileAPI:
9599
"""Encapsulated API logic for file operations."""
96100

src/service/namespace.py

Lines changed: 52 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,77 @@
11
import os
2-
import uuid
32
import json
3+
import shutil
44
from datetime import datetime, timezone
5-
from typing import Callable, Dict, List, Optional
6-
from src.api.api import Namespace, Progress
7-
5+
from typing import List, Optional
86

97
class NamespaceService:
10-
"""Service class for managing namespaces using the filesystem."""
8+
"""Manages namespaces on the filesystem."""
119

1210
def __init__(self, base_dir: Optional[str] = None):
13-
"""
14-
Initialize the NamespaceService.
15-
16-
Args:
17-
base_dir (str): Base directory for namespace storage. Defaults to `$HOME/easevoice_trainer_namespaces`.
18-
"""
19-
if base_dir is None:
20-
home_dir = os.path.expanduser("~")
21-
base_dir = os.path.join(home_dir, "easevoice_trainer_namespaces")
22-
self.base_dir = base_dir
11+
self.base_dir = base_dir or os.path.join(os.path.expanduser("~"), "easevoice_trainer_namespaces")
2312
os.makedirs(self.base_dir, exist_ok=True)
2413

25-
def _namespace_metadata_path(self, namespace_id: str) -> str:
26-
"""Get the path to the namespace metadata file."""
27-
return os.path.join(self.base_dir, namespace_id, ".metadata.json")
28-
29-
def create_namespace(self, name: str) -> Namespace:
30-
"""Create a new namespace."""
31-
namespace_id = str(uuid.uuid4())
32-
namespace_name = f"Namespace-{namespace_id[:8]}"
33-
created_at = datetime.now(timezone.utc)
34-
home_path = os.path.join(self.base_dir, namespace_id)
35-
os.makedirs(home_path, exist_ok=True)
36-
os.makedirs(os.path.join(home_path, "voices"), exist_ok=True)
14+
def _namespace_metadata_path(self, name: str) -> str:
15+
return os.path.join(self.base_dir, name, ".metadata.json")
3716

38-
namespace = Namespace(
39-
namespaceID=namespace_id,
40-
name=namespace_name if name == "" else name,
41-
createdAt=created_at,
42-
homePath=home_path,
43-
)
17+
def create_namespace(self, name: str) -> dict:
18+
"""Create a new namespace, raising FileExistsError if it already exists."""
19+
home_path = os.path.join(self.base_dir, name)
20+
if os.path.exists(home_path):
21+
raise FileExistsError("Namespace already exists")
4422

23+
os.makedirs(os.path.join(home_path, "voices"), exist_ok=True)
24+
namespace = {"name": name, "createdAt": datetime.now(timezone.utc).isoformat(), "homePath": home_path}
4525
self._save_namespace_metadata(namespace)
46-
4726
return namespace
4827

49-
def get_namespaces(self) -> List[Namespace]:
50-
"""Get all namespaces."""
28+
def get_namespaces(self) -> List[dict]:
29+
"""List all namespaces."""
5130
namespaces = []
52-
for namespace_id in os.listdir(self.base_dir):
53-
namespace_path = os.path.join(self.base_dir, namespace_id)
31+
for name in os.listdir(self.base_dir):
32+
namespace_path = os.path.join(self.base_dir, name)
5433
if os.path.isdir(namespace_path):
5534
try:
56-
namespace = self._load_namespace_metadata(namespace_id)
57-
namespaces.append(namespace)
35+
namespaces.append(self._load_namespace_metadata(name))
5836
except FileNotFoundError:
59-
pass # Skip invalid namespaces
37+
pass
6038
return namespaces
6139

62-
def update_namespace(self, namespace_id: str, name: str) -> Namespace:
63-
"""Update an existing namespace."""
64-
namespace = self._load_namespace_metadata(namespace_id)
65-
namespace.name = name
40+
def update_namespace(self, old_name: str, new_name: str) -> dict:
41+
"""Rename a namespace, raising FileExistsError if the new name is taken."""
42+
old_home_path = os.path.join(self.base_dir, old_name)
43+
new_home_path = os.path.join(self.base_dir, new_name)
44+
45+
if not os.path.exists(old_home_path):
46+
raise ValueError("Namespace not found")
47+
48+
if os.path.exists(new_home_path):
49+
raise FileExistsError("Target namespace already exists")
50+
51+
namespace = self._load_namespace_metadata(old_name)
52+
53+
os.rename(old_home_path, new_home_path)
54+
55+
namespace["name"] = new_name
56+
namespace["homePath"] = new_home_path
6657
self._save_namespace_metadata(namespace)
6758
return namespace
6859

69-
def delete_namespace(self, namespace_id: str):
70-
"""Delete a namespace."""
71-
namespace = self._load_namespace_metadata(namespace_id)
72-
self._delete_directory(namespace.homePath)
73-
74-
def _save_namespace_metadata(self, namespace: Namespace):
75-
"""Save namespace metadata to a file."""
76-
metadata_path = self._namespace_metadata_path(namespace.namespaceID)
77-
print(f"""Saving metadata to {metadata_path}""")
78-
with open(metadata_path, "w") as f:
79-
json.dump(namespace.model_dump(), f, default=str)
80-
81-
def _load_namespace_metadata(self, namespace_id: str) -> Namespace:
82-
"""Load namespace metadata from a file."""
83-
metadata_path = self._namespace_metadata_path(namespace_id)
60+
def delete_namespace(self, name: str):
61+
"""Delete a namespace, raising ValueError if it does not exist."""
62+
home_path = os.path.join(self.base_dir, name)
63+
if not os.path.exists(home_path):
64+
raise ValueError("Namespace not found")
65+
66+
shutil.rmtree(home_path)
67+
68+
def _save_namespace_metadata(self, namespace: dict):
69+
with open(self._namespace_metadata_path(namespace["name"]), "w") as f:
70+
json.dump(namespace, f)
71+
72+
def _load_namespace_metadata(self, name: str) -> dict:
73+
metadata_path = self._namespace_metadata_path(name)
8474
if not os.path.exists(metadata_path):
85-
raise ValueError(f"Namespace with ID {namespace_id} not found.")
75+
raise ValueError("Namespace not found")
8676
with open(metadata_path, "r") as f:
87-
data = json.load(f)
88-
data["createdAt"] = datetime.fromisoformat(data["createdAt"])
89-
return Namespace(**data)
90-
91-
def _delete_directory(self, directory_path: str):
92-
"""Delete a directory and its contents."""
93-
if os.path.exists(directory_path):
94-
for root, dirs, files in os.walk(directory_path, topdown=False):
95-
for file in files:
96-
os.remove(os.path.join(root, file))
97-
for dir in dirs:
98-
os.rmdir(os.path.join(root, dir))
99-
os.rmdir(directory_path)
77+
return json.load(f)

0 commit comments

Comments
 (0)