noticecard 0.1.0__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: noticecard
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: NoticeCard server with RESTful API and web UI
5
5
  Author: NoticeCard Team
6
6
  License: MIT
@@ -115,8 +115,7 @@ curl -X POST "http://localhost:3143/cards/" \
115
115
  -H "Content-Type: application/json" \
116
116
  -d '{
117
117
  "title": "会议通知",
118
- "desc1": "下午两点",
119
- "desc2": "一号会议室"
118
+ "description": "明天下午2点在一号会议室"
120
119
  }'
121
120
  ```
122
121
 
@@ -136,8 +135,7 @@ curl "http://localhost:3143/cards/?skip=0&limit=10"
136
135
  |------|------|------|
137
136
  | id | Integer | 主键,自动生成 |
138
137
  | title | String | 标题(必填) |
139
- | desc1 | String | 描述1(可选) |
140
- | desc2 | String | 描述2(可选) |
138
+ | description | String | 描述(可选) |
141
139
  | created_at | DateTime | 创建时间 |
142
140
  | updated_at | DateTime | 更新时间 |
143
141
 
@@ -85,8 +85,7 @@ curl -X POST "http://localhost:3143/cards/" \
85
85
  -H "Content-Type: application/json" \
86
86
  -d '{
87
87
  "title": "会议通知",
88
- "desc1": "下午两点",
89
- "desc2": "一号会议室"
88
+ "description": "明天下午2点在一号会议室"
90
89
  }'
91
90
  ```
92
91
 
@@ -106,8 +105,7 @@ curl "http://localhost:3143/cards/?skip=0&limit=10"
106
105
  |------|------|------|
107
106
  | id | Integer | 主键,自动生成 |
108
107
  | title | String | 标题(必填) |
109
- | desc1 | String | 描述1(可选) |
110
- | desc2 | String | 描述2(可选) |
108
+ | description | String | 描述(可选) |
111
109
  | created_at | DateTime | 创建时间 |
112
110
  | updated_at | DateTime | 更新时间 |
113
111
 
@@ -1,3 +1,3 @@
1
1
  """NoticeCard Server - A FastAPI-based card management system."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.1"
@@ -32,6 +32,10 @@ def create_app(db_path: str) -> FastAPI:
32
32
  """Create and configure FastAPI application."""
33
33
  global _session_maker, _db_path
34
34
 
35
+ # Check and migrate database if needed
36
+ from .migrations import check_and_migrate
37
+ check_and_migrate(db_path)
38
+
35
39
  _db_path = db_path
36
40
  engine = init_db(db_path)
37
41
  _session_maker = get_session_maker(engine)
@@ -81,7 +85,7 @@ def create_app(db_path: str) -> FastAPI:
81
85
  },
82
86
  "serverInfo": {
83
87
  "name": "noticecard",
84
- "version": "0.1.0"
88
+ "version": "0.2.1"
85
89
  }
86
90
  }
87
91
  }
@@ -100,8 +104,7 @@ def create_app(db_path: str) -> FastAPI:
100
104
  "type": "object",
101
105
  "properties": {
102
106
  "title": {"type": "string", "description": "卡片标题"},
103
- "desc1": {"type": "string", "description": "描述1(可选)"},
104
- "desc2": {"type": "string", "description": "描述2(可选)"}
107
+ "description": {"type": "string", "description": "卡片描述(可选)"}
105
108
  },
106
109
  "required": ["title"]
107
110
  }
@@ -136,8 +139,7 @@ def create_app(db_path: str) -> FastAPI:
136
139
  "properties": {
137
140
  "card_id": {"type": "number", "description": "卡片ID"},
138
141
  "title": {"type": "string", "description": "新标题(可选)"},
139
- "desc1": {"type": "string", "description": "新描述1(可选)"},
140
- "desc2": {"type": "string", "description": "新描述2(可选)"}
142
+ "description": {"type": "string", "description": "新描述(可选)"}
141
143
  },
142
144
  "required": ["card_id"]
143
145
  }
@@ -175,7 +177,7 @@ def create_app(db_path: str) -> FastAPI:
175
177
  "result": {
176
178
  "content": [{
177
179
  "type": "text",
178
- "text": f"✅ 创建成功!\nID: {card.id}\n标题: {card.title}\n描述1: {card.desc1 or '无'}\n描述2: {card.desc2 or '无'}"
180
+ "text": f"✅ 创建成功!\nID: {card.id}\n标题: {card.title}\n描述: {card.description or '无'}"
179
181
  }]
180
182
  }
181
183
  }
@@ -217,7 +219,7 @@ def create_app(db_path: str) -> FastAPI:
217
219
  "result": {
218
220
  "content": [{
219
221
  "type": "text",
220
- "text": f"📄 卡片详情\n\nID: {card.id}\n标题: {card.title}\n描述1: {card.desc1 or '无'}\n描述2: {card.desc2 or '无'}\n创建时间: {card.created_at}\n更新时间: {card.updated_at}"
222
+ "text": f"📄 卡片详情\n\nID: {card.id}\n标题: {card.title}\n描述: {card.description or '无'}\n创建时间: {card.created_at}\n更新时间: {card.updated_at}"
221
223
  }]
222
224
  }
223
225
  }
@@ -7,7 +7,7 @@ import uvicorn
7
7
 
8
8
 
9
9
  @click.group()
10
- @click.version_option(version="0.1.0")
10
+ @click.version_option(version="0.2.1")
11
11
  def main():
12
12
  """NoticeCard 服务端命令行工具"""
13
13
  pass
@@ -76,5 +76,25 @@ def init(work_dir: str):
76
76
  click.echo(f"数据库已初始化: {db_path}")
77
77
 
78
78
 
79
+ @main.command()
80
+ @click.option("--dir", "work_dir", default=".", help="工作目录")
81
+ def migrate(work_dir: str):
82
+ """迁移数据库到最新版本"""
83
+ work_path = Path(work_dir).resolve()
84
+ db_path = work_path / "noticecard.db"
85
+
86
+ if not db_path.exists():
87
+ click.echo(f"数据库不存在: {db_path}")
88
+ return
89
+
90
+ from .migrations import migrate_to_v2
91
+
92
+ try:
93
+ migrate_to_v2(str(db_path))
94
+ click.echo(f"✅ 数据库迁移成功: {db_path}")
95
+ except Exception as e:
96
+ click.echo(f"❌ 迁移失败: {e}", err=True)
97
+
98
+
79
99
  if __name__ == "__main__":
80
100
  main()
@@ -0,0 +1,84 @@
1
+ """Database migration utilities."""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+
6
+
7
+ def migrate_to_v2(db_path: str):
8
+ """Migrate database from v1 (desc1, desc2) to v2 (description)."""
9
+ conn = sqlite3.connect(db_path)
10
+ cursor = conn.cursor()
11
+
12
+ try:
13
+ # Check if we need to migrate
14
+ cursor.execute("PRAGMA table_info(cards)")
15
+ columns = [col[1] for col in cursor.fetchall()]
16
+
17
+ if 'description' in columns:
18
+ print("Database already migrated to v2")
19
+ return
20
+
21
+ if 'desc1' not in columns:
22
+ print("Database is already in correct format")
23
+ return
24
+
25
+ print("Migrating database to v2...")
26
+
27
+ # Create new table with description field
28
+ cursor.execute("""
29
+ CREATE TABLE cards_new (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ title TEXT NOT NULL,
32
+ description TEXT,
33
+ created_at TIMESTAMP NOT NULL,
34
+ updated_at TIMESTAMP NOT NULL
35
+ )
36
+ """)
37
+
38
+ # Migrate data: combine desc1 and desc2 into description
39
+ cursor.execute("""
40
+ INSERT INTO cards_new (id, title, description, created_at, updated_at)
41
+ SELECT
42
+ id,
43
+ title,
44
+ CASE
45
+ WHEN desc1 IS NOT NULL AND desc2 IS NOT NULL THEN desc1 || '\n' || desc2
46
+ WHEN desc1 IS NOT NULL THEN desc1
47
+ WHEN desc2 IS NOT NULL THEN desc2
48
+ ELSE NULL
49
+ END as description,
50
+ created_at,
51
+ updated_at
52
+ FROM cards
53
+ """)
54
+
55
+ # Drop old table and rename new one
56
+ cursor.execute("DROP TABLE cards")
57
+ cursor.execute("ALTER TABLE cards_new RENAME TO cards")
58
+
59
+ # Create index
60
+ cursor.execute("CREATE INDEX ix_cards_title ON cards (title)")
61
+ cursor.execute("CREATE INDEX ix_cards_id ON cards (id)")
62
+
63
+ conn.commit()
64
+ print("Migration completed successfully!")
65
+
66
+ except Exception as e:
67
+ conn.rollback()
68
+ print(f"Migration failed: {e}")
69
+ raise
70
+ finally:
71
+ conn.close()
72
+
73
+
74
+ def check_and_migrate(db_path: str):
75
+ """Check database version and migrate if needed."""
76
+ if not Path(db_path).exists():
77
+ # New database, no migration needed
78
+ return
79
+
80
+ try:
81
+ migrate_to_v2(db_path)
82
+ except Exception as e:
83
+ print(f"Warning: Could not migrate database: {e}")
84
+ print("You may need to delete the old database and start fresh.")
@@ -17,8 +17,7 @@ class Card(Base):
17
17
 
18
18
  id = Column(Integer, primary_key=True, index=True, autoincrement=True)
19
19
  title = Column(String, nullable=False, index=True)
20
- desc1 = Column(String, nullable=True)
21
- desc2 = Column(String, nullable=True)
20
+ description = Column(String, nullable=True)
22
21
  created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
23
22
  updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
24
23
 
@@ -8,8 +8,7 @@ from pydantic import BaseModel, Field
8
8
  class CardBase(BaseModel):
9
9
  """Base card schema."""
10
10
  title: str = Field(..., min_length=1, max_length=200, description="Card title")
11
- desc1: Optional[str] = Field(None, max_length=500, description="First description")
12
- desc2: Optional[str] = Field(None, max_length=500, description="Second description")
11
+ description: Optional[str] = Field(None, max_length=1000, description="Card description")
13
12
 
14
13
 
15
14
  class CardCreate(CardBase):
@@ -20,8 +19,7 @@ class CardCreate(CardBase):
20
19
  "examples": [
21
20
  {
22
21
  "title": "会议通知",
23
- "desc1": "下午两点",
24
- "desc2": "一号会议室"
22
+ "description": "明天下午2点在一号会议室"
25
23
  }
26
24
  ]
27
25
  }
@@ -31,15 +29,14 @@ class CardCreate(CardBase):
31
29
  class CardUpdate(BaseModel):
32
30
  """Schema for updating an existing card."""
33
31
  title: Optional[str] = Field(None, min_length=1, max_length=200, description="Card title")
34
- desc1: Optional[str] = Field(None, max_length=500, description="First description")
35
- desc2: Optional[str] = Field(None, max_length=500, description="Second description")
32
+ description: Optional[str] = Field(None, max_length=1000, description="Card description")
36
33
 
37
34
  model_config = {
38
35
  "json_schema_extra": {
39
36
  "examples": [
40
37
  {
41
38
  "title": "更新后的标题",
42
- "desc1": "更新后的描述1"
39
+ "description": "更新后的描述"
43
40
  }
44
41
  ]
45
42
  }
@@ -193,14 +193,9 @@ def get_index_html() -> str:
193
193
  <small id="titleHint" class="form-hint">必填,最多200字符</small>
194
194
  </div>
195
195
  <div class="mb-3">
196
- <label for="desc1" class="form-label">描述1</label>
197
- <textarea class="form-control" id="desc1" name="desc1" rows="2" maxlength="500" aria-describedby="desc1Hint"></textarea>
198
- <small id="desc1Hint" class="form-hint">可选,最多500字符</small>
199
- </div>
200
- <div class="mb-3">
201
- <label for="desc2" class="form-label">描述2</label>
202
- <textarea class="form-control" id="desc2" name="desc2" rows="2" maxlength="500" aria-describedby="desc2Hint"></textarea>
203
- <small id="desc2Hint" class="form-hint">可选,最多500字符</small>
196
+ <label for="description" class="form-label">描述</label>
197
+ <textarea class="form-control" id="description" name="description" rows="4" maxlength="1000" aria-describedby="descriptionHint"></textarea>
198
+ <small id="descriptionHint" class="form-hint">可选,最多1000字符</small>
204
199
  </div>
205
200
  </form>
206
201
  </div>
@@ -260,8 +255,7 @@ def get_index_html() -> str:
260
255
  <article class="notice-card" data-card-id="${card.id}" role="listitem">
261
256
  <div class="card-body">
262
257
  <h3 class="card-title heading">${escapeHtml(card.title)}</h3>
263
- ${card.desc1 ? `<p class="text-muted mb-2">${escapeHtml(card.desc1)}</p>` : ''}
264
- ${card.desc2 ? `<p class="text-muted mb-0">${escapeHtml(card.desc2)}</p>` : ''}
258
+ ${card.description ? `<p class="text-muted mb-0">${escapeHtml(card.description)}</p>` : ''}
265
259
  </div>
266
260
  <div class="card-footer">
267
261
  <div class="d-flex justify-content-between align-items-center">
@@ -308,8 +302,7 @@ def get_index_html() -> str:
308
302
  document.getElementById('modalTitle').textContent = '编辑卡片';
309
303
  document.getElementById('cardId').value = card.id;
310
304
  document.getElementById('title').value = card.title;
311
- document.getElementById('desc1').value = card.desc1 || '';
312
- document.getElementById('desc2').value = card.desc2 || '';
305
+ document.getElementById('description').value = card.description || '';
313
306
 
314
307
  modal.show();
315
308
  } catch (error) {
@@ -329,8 +322,7 @@ def get_index_html() -> str:
329
322
  const id = document.getElementById('cardId').value;
330
323
  const data = {
331
324
  title: document.getElementById('title').value.trim(),
332
- desc1: document.getElementById('desc1').value.trim() || null,
333
- desc2: document.getElementById('desc2').value.trim() || null
325
+ description: document.getElementById('description').value.trim() || null
334
326
  };
335
327
 
336
328
  const spinner = document.getElementById('saveSpinner');
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: noticecard
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: NoticeCard server with RESTful API and web UI
5
5
  Author: NoticeCard Team
6
6
  License: MIT
@@ -115,8 +115,7 @@ curl -X POST "http://localhost:3143/cards/" \
115
115
  -H "Content-Type: application/json" \
116
116
  -d '{
117
117
  "title": "会议通知",
118
- "desc1": "下午两点",
119
- "desc2": "一号会议室"
118
+ "description": "明天下午2点在一号会议室"
120
119
  }'
121
120
  ```
122
121
 
@@ -136,8 +135,7 @@ curl "http://localhost:3143/cards/?skip=0&limit=10"
136
135
  |------|------|------|
137
136
  | id | Integer | 主键,自动生成 |
138
137
  | title | String | 标题(必填) |
139
- | desc1 | String | 描述1(可选) |
140
- | desc2 | String | 描述2(可选) |
138
+ | description | String | 描述(可选) |
141
139
  | created_at | DateTime | 创建时间 |
142
140
  | updated_at | DateTime | 更新时间 |
143
141
 
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  noticecard/__init__.py
4
4
  noticecard/app.py
5
5
  noticecard/cli.py
6
+ noticecard/migrations.py
6
7
  noticecard/models.py
7
8
  noticecard/schemas.py
8
9
  noticecard/templates.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "noticecard"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "NoticeCard server with RESTful API and web UI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes