yeonjae-universal-data-storage 1.0.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.
- yeonjae_universal_data_storage-1.0.1/PKG-INFO +39 -0
- yeonjae_universal_data_storage-1.0.1/README.md +9 -0
- yeonjae_universal_data_storage-1.0.1/pyproject.toml +81 -0
- yeonjae_universal_data_storage-1.0.1/setup.cfg +4 -0
- yeonjae_universal_data_storage-1.0.1/src/universal_data_storage/__init__.py +3 -0
- yeonjae_universal_data_storage-1.0.1/src/universal_data_storage/exceptions.py +92 -0
- yeonjae_universal_data_storage-1.0.1/src/universal_data_storage/models.py +248 -0
- yeonjae_universal_data_storage-1.0.1/src/universal_data_storage/service.py +504 -0
- yeonjae_universal_data_storage-1.0.1/src/yeonjae_universal_data_storage.egg-info/PKG-INFO +39 -0
- yeonjae_universal_data_storage-1.0.1/src/yeonjae_universal_data_storage.egg-info/SOURCES.txt +13 -0
- yeonjae_universal_data_storage-1.0.1/src/yeonjae_universal_data_storage.egg-info/dependency_links.txt +1 -0
- yeonjae_universal_data_storage-1.0.1/src/yeonjae_universal_data_storage.egg-info/requires.txt +11 -0
- yeonjae_universal_data_storage-1.0.1/src/yeonjae_universal_data_storage.egg-info/top_level.txt +1 -0
- yeonjae_universal_data_storage-1.0.1/tests/test_basic.py +98 -0
- yeonjae_universal_data_storage-1.0.1/tests/test_models.py +504 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: yeonjae-universal-data-storage
|
3
|
+
Version: 1.0.1
|
4
|
+
Summary: Universal data storage module for persisting development data
|
5
|
+
Author-email: Yeonjae <dev@example.com>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/yeonjae/universal-modules
|
8
|
+
Project-URL: Repository, https://github.com/yeonjae/universal-modules
|
9
|
+
Project-URL: Issues, https://github.com/yeonjae/universal-modules/issues
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
18
|
+
Requires-Python: >=3.9
|
19
|
+
Description-Content-Type: text/markdown
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
21
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
22
|
+
Provides-Extra: dev
|
23
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
24
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
26
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
27
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
28
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
29
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
30
|
+
|
31
|
+
# Universal data storage
|
32
|
+
|
33
|
+
범용 data storage 모듈
|
34
|
+
|
35
|
+
## 설치
|
36
|
+
|
37
|
+
```bash
|
38
|
+
pip install git+https://github.com/yeonjae-work/universal-modules.git#subdirectory=packages/universal-data-storage
|
39
|
+
```
|
@@ -0,0 +1,81 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "yeonjae-universal-data-storage"
|
7
|
+
version = "1.0.1"
|
8
|
+
description = "Universal data storage module for persisting development data"
|
9
|
+
authors = [{name = "Yeonjae", email = "dev@example.com"}]
|
10
|
+
license = {text = "MIT"}
|
11
|
+
readme = "README.md"
|
12
|
+
requires-python = ">=3.9"
|
13
|
+
classifiers = [
|
14
|
+
"Development Status :: 4 - Beta",
|
15
|
+
"Intended Audience :: Developers",
|
16
|
+
"License :: OSI Approved :: MIT License",
|
17
|
+
"Programming Language :: Python :: 3",
|
18
|
+
"Programming Language :: Python :: 3.9",
|
19
|
+
"Programming Language :: Python :: 3.10",
|
20
|
+
"Programming Language :: Python :: 3.11",
|
21
|
+
"Programming Language :: Python :: 3.12",
|
22
|
+
]
|
23
|
+
dependencies = [
|
24
|
+
"pydantic>=2.0.0",
|
25
|
+
"sqlalchemy>=2.0.0",
|
26
|
+
]
|
27
|
+
|
28
|
+
[project.optional-dependencies]
|
29
|
+
dev = [
|
30
|
+
"pytest>=7.0.0",
|
31
|
+
"pytest-cov>=4.0.0",
|
32
|
+
"pytest-asyncio>=0.21.0",
|
33
|
+
"black>=23.0.0",
|
34
|
+
"isort>=5.12.0",
|
35
|
+
"flake8>=6.0.0",
|
36
|
+
"mypy>=1.0.0",
|
37
|
+
]
|
38
|
+
|
39
|
+
[project.urls]
|
40
|
+
Homepage = "https://github.com/yeonjae/universal-modules"
|
41
|
+
Repository = "https://github.com/yeonjae/universal-modules"
|
42
|
+
Issues = "https://github.com/yeonjae/universal-modules/issues"
|
43
|
+
|
44
|
+
[tool.setuptools.packages.find]
|
45
|
+
where = ["src"]
|
46
|
+
|
47
|
+
[tool.setuptools.package-data]
|
48
|
+
universal_data_storage = ["py.typed"]
|
49
|
+
|
50
|
+
[tool.pytest.ini_options]
|
51
|
+
testpaths = ["tests"]
|
52
|
+
python_files = ["test_*.py"]
|
53
|
+
python_classes = ["Test*"]
|
54
|
+
python_functions = ["test_*"]
|
55
|
+
addopts = "-v --tb=short"
|
56
|
+
|
57
|
+
[tool.coverage.run]
|
58
|
+
source = ["src/universal_data_storage"]
|
59
|
+
omit = ["tests/*"]
|
60
|
+
|
61
|
+
[tool.coverage.report]
|
62
|
+
exclude_lines = [
|
63
|
+
"pragma: no cover",
|
64
|
+
"def __repr__",
|
65
|
+
"raise AssertionError",
|
66
|
+
"raise NotImplementedError",
|
67
|
+
]
|
68
|
+
|
69
|
+
[tool.mypy]
|
70
|
+
python_version = "3.9"
|
71
|
+
warn_return_any = true
|
72
|
+
warn_unused_configs = true
|
73
|
+
disallow_untyped_defs = true
|
74
|
+
|
75
|
+
[tool.black]
|
76
|
+
line-length = 100
|
77
|
+
target-version = ['py39']
|
78
|
+
|
79
|
+
[tool.isort]
|
80
|
+
profile = "black"
|
81
|
+
line_length = 100
|
@@ -0,0 +1,92 @@
|
|
1
|
+
"""
|
2
|
+
Universal Data Storage 예외 클래스
|
3
|
+
|
4
|
+
데이터 저장과 관련된 예외들을 정의합니다.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Dict, Any, Optional
|
8
|
+
|
9
|
+
|
10
|
+
class DataStorageException(Exception):
|
11
|
+
"""데이터 저장 관련 기본 예외 클래스"""
|
12
|
+
|
13
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
14
|
+
self.message = message
|
15
|
+
self.details = details or {}
|
16
|
+
super().__init__(self.message)
|
17
|
+
|
18
|
+
|
19
|
+
class DatabaseConnectionException(DataStorageException):
|
20
|
+
"""데이터베이스 연결 예외"""
|
21
|
+
|
22
|
+
def __init__(self, original_error: Exception):
|
23
|
+
message = "Database connection failed"
|
24
|
+
details = {
|
25
|
+
"error_type": type(original_error).__name__,
|
26
|
+
"error_message": str(original_error)
|
27
|
+
}
|
28
|
+
super().__init__(message, details)
|
29
|
+
|
30
|
+
|
31
|
+
class DuplicateDataException(DataStorageException):
|
32
|
+
"""중복 데이터 예외"""
|
33
|
+
|
34
|
+
def __init__(self, commit_hash: str, existing_data: Dict[str, Any]):
|
35
|
+
message = f"Duplicate commit data found for hash: {commit_hash}"
|
36
|
+
details = {
|
37
|
+
"commit_hash": commit_hash,
|
38
|
+
"existing_data": existing_data
|
39
|
+
}
|
40
|
+
super().__init__(message, details)
|
41
|
+
|
42
|
+
|
43
|
+
class StorageValidationException(DataStorageException):
|
44
|
+
"""저장 데이터 검증 예외"""
|
45
|
+
|
46
|
+
def __init__(self, field: str, value: Any, reason: str):
|
47
|
+
message = f"Storage validation failed for field '{field}': {reason}"
|
48
|
+
details = {
|
49
|
+
"field": field,
|
50
|
+
"value": value,
|
51
|
+
"reason": reason
|
52
|
+
}
|
53
|
+
super().__init__(message, details)
|
54
|
+
|
55
|
+
|
56
|
+
class StorageOperationException(DataStorageException):
|
57
|
+
"""저장 작업 예외"""
|
58
|
+
|
59
|
+
def __init__(self, operation: str, original_error: Exception):
|
60
|
+
message = f"Storage operation '{operation}' failed"
|
61
|
+
details = {
|
62
|
+
"operation": operation,
|
63
|
+
"error_type": type(original_error).__name__,
|
64
|
+
"error_message": str(original_error)
|
65
|
+
}
|
66
|
+
super().__init__(message, details)
|
67
|
+
|
68
|
+
|
69
|
+
class BatchStorageException(DataStorageException):
|
70
|
+
"""배치 저장 예외"""
|
71
|
+
|
72
|
+
def __init__(self, failed_count: int, total_count: int, errors: list):
|
73
|
+
message = f"Batch storage failed: {failed_count}/{total_count} items failed"
|
74
|
+
details = {
|
75
|
+
"failed_count": failed_count,
|
76
|
+
"total_count": total_count,
|
77
|
+
"errors": errors
|
78
|
+
}
|
79
|
+
super().__init__(message, details)
|
80
|
+
|
81
|
+
|
82
|
+
class CompressionException(DataStorageException):
|
83
|
+
"""데이터 압축 예외"""
|
84
|
+
|
85
|
+
def __init__(self, data_type: str, original_error: Exception):
|
86
|
+
message = f"Data compression failed for {data_type}"
|
87
|
+
details = {
|
88
|
+
"data_type": data_type,
|
89
|
+
"error_type": type(original_error).__name__,
|
90
|
+
"error_message": str(original_error)
|
91
|
+
}
|
92
|
+
super().__init__(message, details)
|
@@ -0,0 +1,248 @@
|
|
1
|
+
"""Data storage models for webhook events - MVP Version based on design spec."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from datetime import datetime
|
6
|
+
from typing import Optional, Dict, Any, List
|
7
|
+
from enum import Enum
|
8
|
+
|
9
|
+
from sqlalchemy import (
|
10
|
+
Column, String, DateTime, LargeBinary, Integer,
|
11
|
+
UniqueConstraint, ForeignKey, Text, Index, create_engine
|
12
|
+
)
|
13
|
+
from sqlalchemy.sql import func
|
14
|
+
from sqlalchemy.orm import relationship, declarative_base
|
15
|
+
from pydantic import BaseModel, ConfigDict
|
16
|
+
|
17
|
+
# SQLAlchemy Base 독립적으로 생성
|
18
|
+
Base = declarative_base()
|
19
|
+
|
20
|
+
|
21
|
+
class StorageStatus(str, Enum):
|
22
|
+
"""저장 상태"""
|
23
|
+
SUCCESS = "success"
|
24
|
+
FAILED = "failed"
|
25
|
+
DUPLICATE = "duplicate"
|
26
|
+
PENDING = "pending"
|
27
|
+
|
28
|
+
|
29
|
+
# SQLAlchemy Models (Database Tables)
|
30
|
+
class CommitRecord(Base):
|
31
|
+
"""커밋 정보 테이블 - MVP 버전"""
|
32
|
+
|
33
|
+
__tablename__ = "commits"
|
34
|
+
|
35
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
36
|
+
hash = Column(String(40), unique=True, nullable=False, index=True)
|
37
|
+
message = Column(Text, nullable=False)
|
38
|
+
author = Column(String(255), nullable=False, index=True)
|
39
|
+
author_email = Column(String(255), nullable=True)
|
40
|
+
timestamp = Column(DateTime, nullable=False, index=True)
|
41
|
+
repository = Column(String(255), nullable=False, index=True)
|
42
|
+
branch = Column(String(255), nullable=False)
|
43
|
+
pusher = Column(String(255), nullable=True)
|
44
|
+
commit_count = Column(Integer, nullable=False, default=1)
|
45
|
+
created_at = Column(DateTime, nullable=False, default=func.now())
|
46
|
+
|
47
|
+
# 관계 설정
|
48
|
+
diffs = relationship("DiffRecord", back_populates="commit", cascade="all, delete-orphan")
|
49
|
+
|
50
|
+
def __repr__(self) -> str:
|
51
|
+
return f"<CommitRecord(id={self.id}, hash={self.hash[:8]}, repo={self.repository})>"
|
52
|
+
|
53
|
+
|
54
|
+
class DiffRecord(Base):
|
55
|
+
"""Diff 정보 테이블 - MVP 버전"""
|
56
|
+
|
57
|
+
__tablename__ = "commit_diffs"
|
58
|
+
|
59
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
60
|
+
commit_id = Column(Integer, ForeignKey("commits.id", ondelete="CASCADE"), nullable=False)
|
61
|
+
file_path = Column(Text, nullable=False)
|
62
|
+
additions = Column(Integer, nullable=False, default=0)
|
63
|
+
deletions = Column(Integer, nullable=False, default=0)
|
64
|
+
changes = Column(Text, nullable=True) # diff 내용 (압축된 형태)
|
65
|
+
diff_patch = Column(LargeBinary, nullable=True) # 압축된 diff 데이터
|
66
|
+
diff_url = Column(String, nullable=True) # S3 URL (큰 파일의 경우)
|
67
|
+
created_at = Column(DateTime, nullable=False, default=func.now())
|
68
|
+
|
69
|
+
# 관계 설정
|
70
|
+
commit = relationship("CommitRecord", back_populates="diffs")
|
71
|
+
|
72
|
+
# 인덱스 추가
|
73
|
+
__table_args__ = (
|
74
|
+
Index('idx_diffs_commit_id', 'commit_id'),
|
75
|
+
Index('idx_diffs_file_path', 'file_path'),
|
76
|
+
)
|
77
|
+
|
78
|
+
def __repr__(self) -> str:
|
79
|
+
return f"<DiffRecord(id={self.id}, commit_id={self.commit_id}, file={self.file_path})>"
|
80
|
+
|
81
|
+
|
82
|
+
# Legacy Event Model (기존 호환성 유지)
|
83
|
+
class Event(Base):
|
84
|
+
"""Database model for GitHub webhook events - Legacy support."""
|
85
|
+
|
86
|
+
__tablename__ = "events"
|
87
|
+
|
88
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
89
|
+
platform = Column(String, nullable=False, default="github")
|
90
|
+
repository = Column(String, nullable=False, index=True)
|
91
|
+
commit_sha = Column(String, nullable=False, index=True)
|
92
|
+
author_name = Column(String, nullable=True)
|
93
|
+
author_email = Column(String, nullable=True)
|
94
|
+
timestamp_utc = Column(DateTime, nullable=True)
|
95
|
+
ref = Column(String, nullable=True)
|
96
|
+
pusher = Column(String, nullable=False, index=True)
|
97
|
+
commit_count = Column(Integer, nullable=False, default=1)
|
98
|
+
diff_patch = Column(LargeBinary, nullable=True) # Compressed diff or None if stored in S3
|
99
|
+
diff_url = Column(String, nullable=True) # S3 URL if diff is too large
|
100
|
+
added_lines = Column(Integer, nullable=True)
|
101
|
+
deleted_lines = Column(Integer, nullable=True)
|
102
|
+
files_changed = Column(Integer, nullable=True)
|
103
|
+
payload = Column(String, nullable=False) # JSON string
|
104
|
+
created_at = Column(DateTime, nullable=False, default=func.now())
|
105
|
+
|
106
|
+
# Prevent duplicate events
|
107
|
+
__table_args__ = (
|
108
|
+
UniqueConstraint('repository', 'commit_sha', name='uq_repo_commit'),
|
109
|
+
)
|
110
|
+
|
111
|
+
def __repr__(self) -> str:
|
112
|
+
return f"<Event(id={self.id}, repo={self.repository}, sha={self.commit_sha[:8]})>"
|
113
|
+
|
114
|
+
|
115
|
+
# Pydantic Models (API Request/Response)
|
116
|
+
class CommitData(BaseModel):
|
117
|
+
"""커밋 데이터 입력 모델"""
|
118
|
+
|
119
|
+
commit_hash: str
|
120
|
+
message: str
|
121
|
+
author: str
|
122
|
+
author_email: Optional[str] = None
|
123
|
+
timestamp: datetime
|
124
|
+
repository: str
|
125
|
+
branch: str
|
126
|
+
pusher: Optional[str] = None
|
127
|
+
commit_count: int = 1
|
128
|
+
|
129
|
+
model_config = ConfigDict(
|
130
|
+
from_attributes=True,
|
131
|
+
json_encoders={datetime: lambda v: v.isoformat()}
|
132
|
+
)
|
133
|
+
|
134
|
+
|
135
|
+
class DiffData(BaseModel):
|
136
|
+
"""Diff 데이터 입력 모델"""
|
137
|
+
|
138
|
+
file_path: str
|
139
|
+
additions: int = 0
|
140
|
+
deletions: int = 0
|
141
|
+
changes: Optional[str] = None
|
142
|
+
diff_content: Optional[bytes] = None # 원본 diff 내용
|
143
|
+
|
144
|
+
model_config = ConfigDict(
|
145
|
+
from_attributes=True,
|
146
|
+
arbitrary_types_allowed=True
|
147
|
+
)
|
148
|
+
|
149
|
+
|
150
|
+
class StorageResult(BaseModel):
|
151
|
+
"""저장 결과 응답 모델"""
|
152
|
+
|
153
|
+
success: bool
|
154
|
+
status: StorageStatus
|
155
|
+
commit_id: Optional[int] = None
|
156
|
+
message: str
|
157
|
+
timestamp: datetime
|
158
|
+
metadata: Dict[str, Any] = {}
|
159
|
+
|
160
|
+
model_config = ConfigDict(from_attributes=True)
|
161
|
+
|
162
|
+
|
163
|
+
class CommitSummary(BaseModel):
|
164
|
+
"""커밋 요약 정보"""
|
165
|
+
|
166
|
+
id: int
|
167
|
+
hash: str
|
168
|
+
message: str
|
169
|
+
author: str
|
170
|
+
timestamp: datetime
|
171
|
+
repository: str
|
172
|
+
branch: str
|
173
|
+
diff_count: int
|
174
|
+
total_additions: int
|
175
|
+
total_deletions: int
|
176
|
+
|
177
|
+
model_config = ConfigDict(from_attributes=True)
|
178
|
+
|
179
|
+
|
180
|
+
class DiffSummary(BaseModel):
|
181
|
+
"""Diff 요약 정보"""
|
182
|
+
|
183
|
+
id: int
|
184
|
+
file_path: str
|
185
|
+
additions: int
|
186
|
+
deletions: int
|
187
|
+
has_content: bool
|
188
|
+
|
189
|
+
model_config = ConfigDict(from_attributes=True)
|
190
|
+
|
191
|
+
|
192
|
+
# Legacy Models (기존 호환성)
|
193
|
+
class EventCreate(BaseModel):
|
194
|
+
"""Pydantic model for creating new events."""
|
195
|
+
|
196
|
+
repository: str
|
197
|
+
commit_sha: str
|
198
|
+
event_type: str = "push"
|
199
|
+
payload: str
|
200
|
+
diff_data: Optional[bytes] = None
|
201
|
+
diff_s3_url: Optional[str] = None
|
202
|
+
|
203
|
+
model_config = ConfigDict(
|
204
|
+
from_attributes=True,
|
205
|
+
arbitrary_types_allowed=True
|
206
|
+
)
|
207
|
+
|
208
|
+
|
209
|
+
class EventResponse(BaseModel):
|
210
|
+
"""Pydantic model for event API responses."""
|
211
|
+
|
212
|
+
id: int
|
213
|
+
repository: str
|
214
|
+
commit_sha: str
|
215
|
+
event_type: str
|
216
|
+
payload: Dict[str, Any]
|
217
|
+
diff_s3_url: Optional[str] = None
|
218
|
+
created_at: datetime
|
219
|
+
|
220
|
+
model_config = ConfigDict(from_attributes=True)
|
221
|
+
|
222
|
+
|
223
|
+
# Aggregated Models
|
224
|
+
class CommitWithDiffs(BaseModel):
|
225
|
+
"""커밋과 관련 Diff 정보를 포함한 집계 모델"""
|
226
|
+
|
227
|
+
commit: CommitSummary
|
228
|
+
diffs: List[DiffSummary]
|
229
|
+
|
230
|
+
model_config = ConfigDict(from_attributes=True)
|
231
|
+
|
232
|
+
|
233
|
+
class BatchStorageResult(BaseModel):
|
234
|
+
"""배치 저장 결과"""
|
235
|
+
|
236
|
+
total_commits: int
|
237
|
+
successful_commits: int
|
238
|
+
failed_commits: int
|
239
|
+
results: List[StorageResult]
|
240
|
+
duration_seconds: float
|
241
|
+
|
242
|
+
@property
|
243
|
+
def success_rate(self) -> float:
|
244
|
+
if self.total_commits == 0:
|
245
|
+
return 0.0
|
246
|
+
return self.successful_commits / self.total_commits
|
247
|
+
|
248
|
+
model_config = ConfigDict(from_attributes=True)
|