Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
"pydantic-settings>=2.11.0",
"python-dateutil>=2.9.0.post0",
"python-dotenv>=1.2.1",
"pyyaml>=6.0.2",
Comment thread
Datata1 marked this conversation as resolved.
"typing-extensions>=4.14.0",
"urllib3>=2.4.0",
]
Expand Down
136 changes: 135 additions & 1 deletion src/codesphere/core/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Generic, List, TypeVar
from typing import Any, Generic, List, TypeVar

from pydantic import BaseModel, ConfigDict, RootModel
from pydantic.alias_generators import to_camel

Expand All @@ -20,6 +21,68 @@ class CamelModel(BaseModel):
serialize_by_alias=True,
)

def to_dict(
self, *, by_alias: bool = True, exclude_none: bool = False
) -> dict[str, Any]:
"""Export model as a Python dictionary.

Args:
by_alias: Use camelCase keys (API format) if True, snake_case if False.
exclude_none: Exclude fields with None values if True.

Returns:
Dictionary representation of the model.
"""
return self.model_dump(by_alias=by_alias, exclude_none=exclude_none)

def to_json(
self,
*,
by_alias: bool = True,
exclude_none: bool = False,
indent: int | None = None,
) -> str:
"""Export model as a JSON string.

Args:
by_alias: Use camelCase keys (API format) if True, snake_case if False.
exclude_none: Exclude fields with None values if True.
indent: Number of spaces for indentation. None for compact output.

Returns:
JSON string representation of the model.
"""
return self.model_dump_json(
by_alias=by_alias, exclude_none=exclude_none, indent=indent
)

def to_yaml(self, *, by_alias: bool = True, exclude_none: bool = False) -> str:
"""Export model as a YAML string.

Requires PyYAML to be installed (optional dependency).

Args:
by_alias: Use camelCase keys (API format) if True, snake_case if False.
exclude_none: Exclude fields with None values if True.

Returns:
YAML string representation of the model.

Raises:
ImportError: If PyYAML is not installed.
"""
try:
import yaml
except ImportError:
raise ImportError(
"PyYAML is required for YAML export. "
"Install it with: pip install pyyaml"
)
Comment thread
Datata1 marked this conversation as resolved.
Outdated
data = self.to_dict(by_alias=by_alias, exclude_none=exclude_none)
return yaml.dump(
Comment thread
Datata1 marked this conversation as resolved.
Outdated
data, default_flow_style=False, allow_unicode=True, sort_keys=False
)


class ResourceList(RootModel[List[ModelT]], Generic[ModelT]):
root: List[ModelT]
Expand All @@ -32,3 +95,74 @@ def __getitem__(self, item):

def __len__(self):
return len(self.root)

def to_list(
self, *, by_alias: bool = True, exclude_none: bool = False
) -> list[dict[str, Any]]:
"""Export all items as a list of dictionaries.

Args:
by_alias: Use camelCase keys (API format) if True, snake_case if False.
exclude_none: Exclude fields with None values if True.

Returns:
List of dictionary representations.
"""
return [
item.model_dump(by_alias=by_alias, exclude_none=exclude_none)
if hasattr(item, "model_dump")
else item
for item in self.root
]
Comment thread
Datata1 marked this conversation as resolved.

def to_json(
self,
*,
by_alias: bool = True,
exclude_none: bool = False,
indent: int | None = None,
) -> str:
"""Export all items as a JSON array string.

Args:
by_alias: Use camelCase keys (API format) if True, snake_case if False.
exclude_none: Exclude fields with None values if True.
indent: Number of spaces for indentation. None for compact output.

Returns:
JSON array string representation.
"""
import json

return json.dumps(
self.to_list(by_alias=by_alias, exclude_none=exclude_none), indent=indent
)
Comment thread
Datata1 marked this conversation as resolved.

def to_yaml(self, *, by_alias: bool = True, exclude_none: bool = False) -> str:
"""Export all items as a YAML string.

Requires PyYAML to be installed (optional dependency).

Args:
by_alias: Use camelCase keys (API format) if True, snake_case if False.
exclude_none: Exclude fields with None values if True.

Returns:
YAML string representation.

Raises:
ImportError: If PyYAML is not installed.
"""
try:
import yaml
except ImportError:
raise ImportError(
"PyYAML is required for YAML export. "
"Install it with: pip install pyyaml"
)
return yaml.dump(
self.to_list(by_alias=by_alias, exclude_none=exclude_none),
default_flow_style=False,
allow_unicode=True,
sort_keys=False,
)
Comment thread
Datata1 marked this conversation as resolved.
160 changes: 159 additions & 1 deletion tests/core/test_base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from dataclasses import dataclass
from unittest.mock import MagicMock

import pytest
from pydantic import BaseModel

from codesphere.core.base import CamelModel, ResourceBase, ResourceList
Expand Down Expand Up @@ -91,6 +91,96 @@ class SampleModel(CamelModel):
assert model.is_private is False


class TestCamelModelExport:
"""Tests for CamelModel export methods."""

def test_to_dict_default(self):
"""to_dict should export with camelCase keys by default."""

class SampleModel(CamelModel):
team_id: int
user_name: str

model = SampleModel(team_id=1, user_name="test")
result = model.to_dict()

assert result == {"teamId": 1, "userName": "test"}

def test_to_dict_snake_case(self):
"""to_dict with by_alias=False should export with snake_case keys."""

class SampleModel(CamelModel):
team_id: int
user_name: str

model = SampleModel(team_id=1, user_name="test")
result = model.to_dict(by_alias=False)

assert result == {"team_id": 1, "user_name": "test"}

def test_to_dict_exclude_none(self):
"""to_dict with exclude_none=True should omit None values."""

class SampleModel(CamelModel):
team_id: int
optional_field: str | None = None

model = SampleModel(team_id=1, optional_field=None)
result = model.to_dict(exclude_none=True)

assert result == {"teamId": 1}
assert "optionalField" not in result

def test_to_json_default(self):
"""to_json should export as JSON string with camelCase keys."""

class SampleModel(CamelModel):
team_id: int

model = SampleModel(team_id=42)
result = model.to_json()

assert result == '{"teamId":42}'
Comment thread
Datata1 marked this conversation as resolved.
Outdated

def test_to_json_with_indent(self):
"""to_json with indent should format output."""

class SampleModel(CamelModel):
team_id: int

model = SampleModel(team_id=42)
result = model.to_json(indent=2)

assert '"teamId": 42' in result
assert "\n" in result

def test_to_yaml_import_error(self):
"""to_yaml should raise ImportError if PyYAML is not installed."""
import sys
from unittest.mock import patch

class SampleModel(CamelModel):
team_id: int

model = SampleModel(team_id=1)

with patch.dict(sys.modules, {"yaml": None}):
# Force reimport to trigger ImportError
with pytest.raises(ImportError, match="PyYAML is required"):
# We need to actually make the import fail
import builtins

original_import = builtins.__import__

def mock_import(name, *args, **kwargs):
if name == "yaml":
raise ImportError("No module named 'yaml'")
return original_import(name, *args, **kwargs)

with patch.object(builtins, "__import__", mock_import):
model.to_yaml()


Comment thread
Datata1 marked this conversation as resolved.
Outdated
class TestResourceList:
def test_create_with_list(self):
"""ResourceList should be created with a list of items."""
Expand Down Expand Up @@ -150,6 +240,74 @@ class Item(BaseModel):
assert list(resource_list) == []


class TestResourceListExport:
"""Tests for ResourceList export methods."""

def test_to_list_default(self):
"""to_list should export items as list of dicts with camelCase keys."""

class Item(CamelModel):
item_id: int
item_name: str

items = [Item(item_id=1, item_name="a"), Item(item_id=2, item_name="b")]
resource_list = ResourceList[Item](root=items)
result = resource_list.to_list()

assert result == [
{"itemId": 1, "itemName": "a"},
{"itemId": 2, "itemName": "b"},
]

def test_to_list_snake_case(self):
"""to_list with by_alias=False should use snake_case keys."""

class Item(CamelModel):
item_id: int

items = [Item(item_id=1)]
resource_list = ResourceList[Item](root=items)
result = resource_list.to_list(by_alias=False)

assert result == [{"item_id": 1}]

def test_to_json_default(self):
"""to_json should export as JSON array string."""

class Item(CamelModel):
item_id: int

items = [Item(item_id=1), Item(item_id=2)]
resource_list = ResourceList[Item](root=items)
result = resource_list.to_json()

assert result == '[{"itemId": 1}, {"itemId": 2}]'
Comment thread
Datata1 marked this conversation as resolved.
Outdated

def test_to_json_with_indent(self):
"""to_json with indent should format output."""

class Item(CamelModel):
item_id: int

items = [Item(item_id=1)]
resource_list = ResourceList[Item](root=items)
result = resource_list.to_json(indent=2)

assert "\n" in result
assert '"itemId": 1' in result

def test_to_list_empty(self):
"""to_list should handle empty lists."""

class Item(CamelModel):
item_id: int

resource_list = ResourceList[Item](root=[])
result = resource_list.to_list()

assert result == []


class TestResourceBase:
def test_initialization_with_http_client(self):
"""ResourceBase should store the HTTP client."""
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.