cinchdb 0.1.0__tar.gz → 0.1.1__tar.gz

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 (69) hide show
  1. {cinchdb-0.1.0 → cinchdb-0.1.1}/PKG-INFO +3 -3
  2. {cinchdb-0.1.0 → cinchdb-0.1.1}/README.md +2 -2
  3. {cinchdb-0.1.0 → cinchdb-0.1.1}/pyproject.toml +1 -1
  4. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/databases.py +7 -0
  5. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/branch.py +9 -0
  6. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/database.py +9 -0
  7. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/tenant.py +17 -0
  8. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/branch.py +5 -0
  9. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/tenant.py +13 -0
  10. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/branch.py +9 -1
  11. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/database.py +9 -1
  12. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/tenant.py +9 -1
  13. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/utils/__init__.py +11 -1
  14. cinchdb-0.1.1/src/cinchdb/utils/name_validator.py +129 -0
  15. {cinchdb-0.1.0 → cinchdb-0.1.1}/.gitignore +0 -0
  16. {cinchdb-0.1.0 → cinchdb-0.1.1}/LICENSE +0 -0
  17. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/__init__.py +0 -0
  18. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/__main__.py +0 -0
  19. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/__init__.py +0 -0
  20. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/app.py +0 -0
  21. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/auth.py +0 -0
  22. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/main.py +0 -0
  23. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/__init__.py +0 -0
  24. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/auth.py +0 -0
  25. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/branches.py +0 -0
  26. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/codegen.py +0 -0
  27. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/columns.py +0 -0
  28. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/data.py +0 -0
  29. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/projects.py +0 -0
  30. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/query.py +0 -0
  31. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/tables.py +0 -0
  32. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/tenants.py +0 -0
  33. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/api/routers/views.py +0 -0
  34. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/__init__.py +0 -0
  35. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/__init__.py +0 -0
  36. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/codegen.py +0 -0
  37. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/column.py +0 -0
  38. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/query.py +0 -0
  39. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/remote.py +0 -0
  40. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/table.py +0 -0
  41. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/commands/view.py +0 -0
  42. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/handlers/__init__.py +0 -0
  43. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
  44. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/main.py +0 -0
  45. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/cli/utils.py +0 -0
  46. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/config.py +0 -0
  47. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/core/__init__.py +0 -0
  48. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/core/connection.py +0 -0
  49. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/core/database.py +0 -0
  50. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/core/maintenance.py +0 -0
  51. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/core/path_utils.py +0 -0
  52. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/__init__.py +0 -0
  53. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/change_applier.py +0 -0
  54. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/change_comparator.py +0 -0
  55. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/change_tracker.py +0 -0
  56. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/codegen.py +0 -0
  57. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/column.py +0 -0
  58. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/data.py +0 -0
  59. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/merge_manager.py +0 -0
  60. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/query.py +0 -0
  61. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/table.py +0 -0
  62. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/managers/view.py +0 -0
  63. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/__init__.py +0 -0
  64. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/base.py +0 -0
  65. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/change.py +0 -0
  66. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/project.py +0 -0
  67. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/table.py +0 -0
  68. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/models/view.py +0 -0
  69. {cinchdb-0.1.0 → cinchdb-0.1.1}/src/cinchdb/utils/sql_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cinchdb
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A Git-like SQLite database management system with branching and multi-tenancy
5
5
  Project-URL: Homepage, https://github.com/russellromney/cinchdb
6
6
  Project-URL: Documentation, https://russellromney.github.io/cinchdb
@@ -34,11 +34,11 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
36
36
 
37
- CinchDB is for projects that need to isolate data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4) and/or want to test out changes to structure safely in branchces. Plus, queries can be super, super fast because data is separated per tenant so it's <data_size>/<n_tenants> instead of <data_size>
37
+ CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database.
38
38
 
39
39
  On a meta level, I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
40
40
 
41
- Because it's so lightweight and its only dependencies are FastAPI, pydantic, requests, and Typer, it is super cheap to run on a small VM like from [Fly.io](https://fly.io) and I don't need to SSH into it to connect - I can just store an API key in my local projects.
41
+ Because it's so lightweight and its only dependencies are FastAPI, pydantic, requests, and Typer, it is super cheap to run on a small VM like from [Fly.io](https://fly.io) and I don't need to SSH into it to make changes - I can just store an API key in my local projects and hit the remote instance.
42
42
 
43
43
 
44
44
  ```bash
@@ -4,11 +4,11 @@
4
4
 
5
5
  NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
6
6
 
7
- CinchDB is for projects that need to isolate data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4) and/or want to test out changes to structure safely in branchces. Plus, queries can be super, super fast because data is separated per tenant so it's <data_size>/<n_tenants> instead of <data_size>
7
+ CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database.
8
8
 
9
9
  On a meta level, I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
10
10
 
11
- Because it's so lightweight and its only dependencies are FastAPI, pydantic, requests, and Typer, it is super cheap to run on a small VM like from [Fly.io](https://fly.io) and I don't need to SSH into it to connect - I can just store an API key in my local projects.
11
+ Because it's so lightweight and its only dependencies are FastAPI, pydantic, requests, and Typer, it is super cheap to run on a small VM like from [Fly.io](https://fly.io) and I don't need to SSH into it to make changes - I can just store an API key in my local projects and hit the remote instance.
12
12
 
13
13
 
14
14
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cinchdb"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "A Git-like SQLite database management system with branching and multi-tenancy"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -11,6 +11,7 @@ from cinchdb.api.auth import (
11
11
  require_write_permission,
12
12
  require_read_permission,
13
13
  )
14
+ from cinchdb.utils.name_validator import validate_name, InvalidNameError
14
15
 
15
16
 
16
17
  router = APIRouter()
@@ -68,6 +69,12 @@ async def create_database(
68
69
  auth: AuthContext = Depends(require_write_permission),
69
70
  ):
70
71
  """Create a new database."""
72
+ # Validate database name
73
+ try:
74
+ validate_name(request.name, "database")
75
+ except InvalidNameError as e:
76
+ raise HTTPException(status_code=400, detail=str(e))
77
+
71
78
  config = Config(auth.project_dir)
72
79
 
73
80
  # Create database directory structure
@@ -14,6 +14,7 @@ from cinchdb.cli.utils import (
14
14
  set_active_branch,
15
15
  validate_required_arg,
16
16
  )
17
+ from cinchdb.utils.name_validator import validate_name, InvalidNameError
17
18
 
18
19
  app = typer.Typer(help="Branch management commands", invoke_without_command=True)
19
20
  console = Console()
@@ -80,6 +81,14 @@ def create(
80
81
  ):
81
82
  """Create a new branch."""
82
83
  name = validate_required_arg(name, "name", ctx)
84
+
85
+ # Validate branch name
86
+ try:
87
+ validate_name(name, "branch")
88
+ except InvalidNameError as e:
89
+ console.print(f"[red]❌ {e}[/red]")
90
+ raise typer.Exit(1)
91
+
83
92
  config, config_data = get_config_with_data()
84
93
  db_name = config_data.active_database
85
94
  source_branch = source or config_data.active_branch
@@ -12,6 +12,7 @@ from cinchdb.cli.utils import (
12
12
  set_active_branch,
13
13
  validate_required_arg,
14
14
  )
15
+ from cinchdb.utils.name_validator import validate_name, InvalidNameError
15
16
 
16
17
  app = typer.Typer(help="Database management commands", invoke_without_command=True)
17
18
  console = Console()
@@ -64,6 +65,14 @@ def create(
64
65
  ):
65
66
  """Create a new database."""
66
67
  name = validate_required_arg(name, "name", ctx)
68
+
69
+ # Validate database name
70
+ try:
71
+ validate_name(name, "database")
72
+ except InvalidNameError as e:
73
+ console.print(f"[red]❌ {e}[/red]")
74
+ raise typer.Exit(1)
75
+
67
76
  config, config_data = get_config_with_data()
68
77
 
69
78
  # Create database directory structure
@@ -10,6 +10,7 @@ from cinchdb.config import Config
10
10
  from cinchdb.core.path_utils import get_project_root
11
11
  from cinchdb.managers.tenant import TenantManager
12
12
  from cinchdb.cli.utils import get_config_with_data, validate_required_arg
13
+ from cinchdb.utils.name_validator import validate_name, InvalidNameError
13
14
 
14
15
  app = typer.Typer(help="Tenant management commands", invoke_without_command=True)
15
16
  console = Console()
@@ -70,6 +71,14 @@ def create(
70
71
  ):
71
72
  """Create a new tenant."""
72
73
  name = validate_required_arg(name, "name", ctx)
74
+
75
+ # Validate tenant name
76
+ try:
77
+ validate_name(name, "tenant")
78
+ except InvalidNameError as e:
79
+ console.print(f"[red]❌ {e}[/red]")
80
+ raise typer.Exit(1)
81
+
73
82
  config, config_data = get_config_with_data()
74
83
  db_name = config_data.active_database
75
84
  branch_name = config_data.active_branch
@@ -155,6 +164,14 @@ def rename(
155
164
  """Rename a tenant."""
156
165
  old_name = validate_required_arg(old_name, "old_name", ctx)
157
166
  new_name = validate_required_arg(new_name, "new_name", ctx)
167
+
168
+ # Validate new tenant name
169
+ try:
170
+ validate_name(new_name, "tenant")
171
+ except InvalidNameError as e:
172
+ console.print(f"[red]❌ {e}[/red]")
173
+ raise typer.Exit(1)
174
+
158
175
  config, config_data = get_config_with_data()
159
176
  db_name = config_data.active_database
160
177
  branch_name = config_data.active_branch
@@ -13,6 +13,7 @@ from cinchdb.core.path_utils import (
13
13
  get_branch_path,
14
14
  list_branches,
15
15
  )
16
+ from cinchdb.utils.name_validator import validate_name
16
17
 
17
18
 
18
19
  class BranchManager:
@@ -63,7 +64,11 @@ class BranchManager:
63
64
 
64
65
  Raises:
65
66
  ValueError: If source doesn't exist or new branch already exists
67
+ InvalidNameError: If new branch name is invalid
66
68
  """
69
+ # Validate new branch name
70
+ validate_name(new_branch_name, "branch")
71
+
67
72
  # Validate source branch exists
68
73
  if source_branch not in list_branches(self.project_root, self.database):
69
74
  raise ValueError(f"Source branch '{source_branch}' does not exist")
@@ -12,6 +12,7 @@ from cinchdb.core.path_utils import (
12
12
  )
13
13
  from cinchdb.core.connection import DatabaseConnection
14
14
  from cinchdb.core.maintenance import check_maintenance_mode
15
+ from cinchdb.utils.name_validator import validate_name
15
16
 
16
17
 
17
18
  class TenantManager:
@@ -64,8 +65,12 @@ class TenantManager:
64
65
 
65
66
  Raises:
66
67
  ValueError: If tenant already exists
68
+ InvalidNameError: If tenant name is invalid
67
69
  MaintenanceError: If branch is in maintenance mode
68
70
  """
71
+ # Validate tenant name
72
+ validate_name(tenant_name, "tenant")
73
+
69
74
  # Check maintenance mode
70
75
  check_maintenance_mode(self.project_root, self.database, self.branch)
71
76
 
@@ -158,8 +163,12 @@ class TenantManager:
158
163
 
159
164
  Raises:
160
165
  ValueError: If source doesn't exist or target already exists
166
+ InvalidNameError: If target tenant name is invalid
161
167
  MaintenanceError: If branch is in maintenance mode
162
168
  """
169
+ # Validate target tenant name
170
+ validate_name(target_tenant, "tenant")
171
+
163
172
  # Check maintenance mode
164
173
  check_maintenance_mode(self.project_root, self.database, self.branch)
165
174
 
@@ -200,7 +209,11 @@ class TenantManager:
200
209
 
201
210
  Raises:
202
211
  ValueError: If old doesn't exist, new already exists, or trying to rename main
212
+ InvalidNameError: If new tenant name is invalid
203
213
  """
214
+ # Validate new tenant name
215
+ validate_name(new_name, "tenant")
216
+
204
217
  # Can't rename main tenant
205
218
  if old_name == "main":
206
219
  raise ValueError("Cannot rename the main tenant")
@@ -1,8 +1,9 @@
1
1
  """Branch model for CinchDB."""
2
2
 
3
3
  from typing import List, Optional, Dict, Any
4
- from pydantic import Field
4
+ from pydantic import Field, field_validator
5
5
  from .base import CinchDBBaseModel
6
+ from ..utils.name_validator import validate_name
6
7
 
7
8
 
8
9
  class Branch(CinchDBBaseModel):
@@ -21,6 +22,13 @@ class Branch(CinchDBBaseModel):
21
22
  )
22
23
  is_main: bool = Field(default=False, description="Whether this is the main branch")
23
24
 
25
+ @field_validator('name')
26
+ @classmethod
27
+ def validate_name_field(cls, v: str) -> str:
28
+ """Validate branch name meets naming requirements."""
29
+ validate_name(v, "branch")
30
+ return v
31
+
24
32
  def can_delete(self) -> bool:
25
33
  """Check if this branch can be deleted."""
26
34
  return self.name != "main" and not self.is_main
@@ -1,8 +1,9 @@
1
1
  """Database model for CinchDB."""
2
2
 
3
3
  from typing import List, Optional
4
- from pydantic import Field
4
+ from pydantic import Field, field_validator
5
5
  from .base import CinchDBBaseModel
6
+ from ..utils.name_validator import validate_name
6
7
 
7
8
 
8
9
  class Database(CinchDBBaseModel):
@@ -15,6 +16,13 @@ class Database(CinchDBBaseModel):
15
16
  active_branch: str = Field(default="main", description="Currently active branch")
16
17
  description: Optional[str] = Field(default=None, description="Database description")
17
18
 
19
+ @field_validator('name')
20
+ @classmethod
21
+ def validate_name_field(cls, v: str) -> str:
22
+ """Validate database name meets naming requirements."""
23
+ validate_name(v, "database")
24
+ return v
25
+
18
26
  def can_delete(self) -> bool:
19
27
  """Check if this database can be deleted."""
20
28
  return self.name != "main"
@@ -1,8 +1,9 @@
1
1
  """Tenant model for CinchDB."""
2
2
 
3
3
  from typing import Optional
4
- from pydantic import Field
4
+ from pydantic import Field, field_validator
5
5
  from .base import CinchDBBaseModel
6
+ from ..utils.name_validator import validate_name
6
7
 
7
8
 
8
9
  class Tenant(CinchDBBaseModel):
@@ -14,6 +15,13 @@ class Tenant(CinchDBBaseModel):
14
15
  description: Optional[str] = Field(default=None, description="Tenant description")
15
16
  is_main: bool = Field(default=False, description="Whether this is the main tenant")
16
17
 
18
+ @field_validator('name')
19
+ @classmethod
20
+ def validate_name_field(cls, v: str) -> str:
21
+ """Validate tenant name meets naming requirements."""
22
+ validate_name(v, "tenant")
23
+ return v
24
+
17
25
  def can_delete(self) -> bool:
18
26
  """Check if this tenant can be deleted."""
19
27
  return self.name != "main" and not self.is_main
@@ -6,10 +6,20 @@ from cinchdb.utils.sql_validator import (
6
6
  SQLValidationError,
7
7
  SQLOperation
8
8
  )
9
+ from cinchdb.utils.name_validator import (
10
+ validate_name,
11
+ clean_name,
12
+ is_valid_name,
13
+ InvalidNameError
14
+ )
9
15
 
10
16
  __all__ = [
11
17
  "validate_sql_query",
12
18
  "validate_query_safe",
13
19
  "SQLValidationError",
14
- "SQLOperation"
20
+ "SQLOperation",
21
+ "validate_name",
22
+ "clean_name",
23
+ "is_valid_name",
24
+ "InvalidNameError"
15
25
  ]
@@ -0,0 +1,129 @@
1
+ """Name validation utilities for CinchDB entities.
2
+
3
+ Ensures branch, database, and tenant names are safe for filesystem operations
4
+ and follow consistent naming conventions.
5
+ """
6
+
7
+ import re
8
+ from typing import Optional
9
+
10
+
11
+ # Regex pattern for valid names: lowercase letters, numbers, dash, underscore, period
12
+ VALID_NAME_PATTERN = re.compile(r'^[a-z0-9][a-z0-9\-._]*[a-z0-9]$|^[a-z0-9]$')
13
+
14
+ # Reserved names that cannot be used
15
+ RESERVED_NAMES = {'con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4',
16
+ 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2',
17
+ 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'}
18
+
19
+
20
+ class InvalidNameError(ValueError):
21
+ """Raised when a name doesn't meet validation requirements."""
22
+ pass
23
+
24
+
25
+ def validate_name(name: str, entity_type: str = "entity") -> None:
26
+ """Validate that a name meets CinchDB naming requirements.
27
+
28
+ Valid names must:
29
+ - Contain only lowercase letters (a-z), numbers (0-9), dash (-), underscore (_), and period (.)
30
+ - Start and end with alphanumeric characters
31
+ - Be at least 1 character long
32
+ - Not exceed 255 characters (filesystem limit)
33
+ - Not be a reserved name
34
+
35
+ Args:
36
+ name: The name to validate
37
+ entity_type: Type of entity (branch, database, tenant) for error messages
38
+
39
+ Raises:
40
+ InvalidNameError: If the name is invalid
41
+ """
42
+ if not name:
43
+ raise InvalidNameError(f"{entity_type.capitalize()} name cannot be empty")
44
+
45
+ if len(name) > 255:
46
+ raise InvalidNameError(
47
+ f"{entity_type.capitalize()} name cannot exceed 255 characters"
48
+ )
49
+
50
+ # Check for lowercase requirement
51
+ if name != name.lower():
52
+ raise InvalidNameError(
53
+ f"{entity_type.capitalize()} name must be lowercase. "
54
+ f"Use '{name.lower()}' instead of '{name}'"
55
+ )
56
+
57
+ # Check pattern
58
+ if not VALID_NAME_PATTERN.match(name):
59
+ raise InvalidNameError(
60
+ f"Invalid {entity_type} name '{name}'. "
61
+ f"Names must contain only lowercase letters (a-z), numbers (0-9), "
62
+ f"dash (-), underscore (_), and period (.). "
63
+ f"Names must start and end with alphanumeric characters."
64
+ )
65
+
66
+ # Check for consecutive special characters
67
+ if '..' in name or '--' in name or '__' in name or '.-' in name or '-.' in name:
68
+ raise InvalidNameError(
69
+ f"Invalid {entity_type} name '{name}'. "
70
+ f"Names cannot contain consecutive special characters."
71
+ )
72
+
73
+ # Check reserved names
74
+ if name.lower() in RESERVED_NAMES:
75
+ raise InvalidNameError(
76
+ f"'{name}' is a reserved name and cannot be used as a {entity_type} name"
77
+ )
78
+
79
+
80
+ def clean_name(name: str) -> str:
81
+ """Clean a name to make it valid if possible.
82
+
83
+ This performs basic cleaning:
84
+ - Convert to lowercase
85
+ - Replace spaces with dashes
86
+ - Remove invalid characters
87
+
88
+ Args:
89
+ name: The name to clean
90
+
91
+ Returns:
92
+ Cleaned name
93
+
94
+ Note:
95
+ This is a best-effort cleaning. The result should still be validated
96
+ with validate_name() before use.
97
+ """
98
+ # Convert to lowercase
99
+ cleaned = name.lower()
100
+
101
+ # Replace spaces with dashes
102
+ cleaned = cleaned.replace(' ', '-')
103
+
104
+ # Remove invalid characters
105
+ cleaned = re.sub(r'[^a-z0-9\-._]', '', cleaned)
106
+
107
+ # Remove consecutive special characters
108
+ cleaned = re.sub(r'[-._]{2,}', '-', cleaned)
109
+
110
+ # Remove leading/trailing special characters
111
+ cleaned = cleaned.strip('-._')
112
+
113
+ return cleaned
114
+
115
+
116
+ def is_valid_name(name: str) -> bool:
117
+ """Check if a name is valid without raising an exception.
118
+
119
+ Args:
120
+ name: The name to check
121
+
122
+ Returns:
123
+ True if valid, False otherwise
124
+ """
125
+ try:
126
+ validate_name(name)
127
+ return True
128
+ except InvalidNameError:
129
+ return False
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes