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.
@@ -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,9 @@
1
+ # Universal data storage
2
+
3
+ 범용 data storage 모듈
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ pip install git+https://github.com/yeonjae-work/universal-modules.git#subdirectory=packages/universal-data-storage
9
+ ```
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Universal Data Storage - 커밋 데이터와 집계 결과를 데이터베이스에 저장하고 관리하는 범용 모듈"""
2
+
3
+ __version__ = "1.0.0"
@@ -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)