staran 1.0.9__py3-none-any.whl → 1.0.11__py3-none-any.whl

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,754 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Staran REST API 服务模块 v1.0.10
6
+ ==============================
7
+
8
+ 提供HTTP API服务支持,用于远程调用Staran日期处理功能。
9
+
10
+ 主要功能:
11
+ - REST API 服务器
12
+ - 日期处理API端点
13
+ - 数据验证和错误处理
14
+ - API文档生成
15
+ - 多种响应格式支持
16
+ """
17
+
18
+ import json
19
+ import datetime
20
+ import traceback
21
+ from typing import Dict, List, Any, Optional, Union
22
+ from dataclasses import dataclass, asdict
23
+ from http.server import HTTPServer, BaseHTTPRequestHandler
24
+ from urllib.parse import urlparse, parse_qs
25
+ import threading
26
+ import logging
27
+
28
+ # 配置日志
29
+ logging.basicConfig(level=logging.INFO)
30
+ logger = logging.getLogger(__name__)
31
+
32
+ @dataclass
33
+ class APIResponse:
34
+ """API响应类"""
35
+ success: bool
36
+ data: Any = None
37
+ error: Optional[str] = None
38
+ error_code: Optional[str] = None
39
+ timestamp: Optional[str] = None
40
+
41
+ def __post_init__(self):
42
+ if self.timestamp is None:
43
+ self.timestamp = datetime.datetime.now().isoformat()
44
+
45
+ class StaranAPIHandler(BaseHTTPRequestHandler):
46
+ """Staran API 请求处理器"""
47
+
48
+ def __init__(self, *args, **kwargs):
49
+ # 导入核心模块
50
+ try:
51
+ from .core import Date
52
+ from .lunar import LunarDate
53
+ from .solar_terms import SolarTerms
54
+ from .timezone import Timezone
55
+ from .expressions import DateExpressionParser
56
+ from .visualization import DateVisualization
57
+
58
+ self.Date = Date
59
+ self.LunarDate = LunarDate
60
+ self.SolarTerms = SolarTerms
61
+ self.Timezone = Timezone
62
+ self.DateExpressionParser = DateExpressionParser
63
+ self.DateVisualization = DateVisualization
64
+ except ImportError as e:
65
+ logger.error(f"导入模块失败: {e}")
66
+
67
+ super().__init__(*args, **kwargs)
68
+
69
+ def do_GET(self):
70
+ """处理GET请求"""
71
+ try:
72
+ parsed_path = urlparse(self.path)
73
+ path = parsed_path.path
74
+ query_params = parse_qs(parsed_path.query)
75
+
76
+ # 路由分发
77
+ if path == '/':
78
+ self._handle_root()
79
+ elif path == '/api/health':
80
+ self._handle_health()
81
+ elif path == '/api/date/create':
82
+ self._handle_date_create(query_params)
83
+ elif path == '/api/date/format':
84
+ self._handle_date_format(query_params)
85
+ elif path == '/api/date/calculate':
86
+ self._handle_date_calculate(query_params)
87
+ elif path == '/api/lunar/convert':
88
+ self._handle_lunar_convert(query_params)
89
+ elif path == '/api/solar-terms':
90
+ self._handle_solar_terms(query_params)
91
+ elif path == '/api/timezone/convert':
92
+ self._handle_timezone_convert(query_params)
93
+ elif path == '/api/expression/parse':
94
+ self._handle_expression_parse(query_params)
95
+ elif path == '/api/visualization/data':
96
+ self._handle_visualization_data(query_params)
97
+ elif path == '/api/docs':
98
+ self._handle_api_docs()
99
+ else:
100
+ self._send_error_response(404, "API_NOT_FOUND", "API端点不存在")
101
+
102
+ except Exception as e:
103
+ logger.error(f"处理GET请求时出错: {e}")
104
+ self._send_error_response(500, "INTERNAL_ERROR", str(e))
105
+
106
+ def do_POST(self):
107
+ """处理POST请求"""
108
+ try:
109
+ parsed_path = urlparse(self.path)
110
+ path = parsed_path.path
111
+
112
+ # 读取请求体
113
+ content_length = int(self.headers.get('Content-Length', 0))
114
+ post_data = self.rfile.read(content_length).decode('utf-8')
115
+
116
+ try:
117
+ request_data = json.loads(post_data) if post_data else {}
118
+ except json.JSONDecodeError:
119
+ self._send_error_response(400, "INVALID_JSON", "无效的JSON格式")
120
+ return
121
+
122
+ # 路由分发
123
+ if path == '/api/date/batch':
124
+ self._handle_date_batch(request_data)
125
+ elif path == '/api/visualization/create':
126
+ self._handle_visualization_create(request_data)
127
+ else:
128
+ self._send_error_response(404, "API_NOT_FOUND", "API端点不存在")
129
+
130
+ except Exception as e:
131
+ logger.error(f"处理POST请求时出错: {e}")
132
+ self._send_error_response(500, "INTERNAL_ERROR", str(e))
133
+
134
+ def do_OPTIONS(self):
135
+ """处理OPTIONS请求(CORS预检)"""
136
+ self._send_cors_headers()
137
+ self.send_response(200)
138
+ self.end_headers()
139
+
140
+ def _send_response(self, response: APIResponse, status_code: int = 200):
141
+ """发送API响应"""
142
+ self._send_cors_headers()
143
+ self.send_response(status_code)
144
+ self.send_header('Content-Type', 'application/json; charset=utf-8')
145
+ self.end_headers()
146
+
147
+ response_json = json.dumps(asdict(response), ensure_ascii=False, indent=2)
148
+ self.wfile.write(response_json.encode('utf-8'))
149
+
150
+ def _send_error_response(self, status_code: int, error_code: str, error_message: str):
151
+ """发送错误响应"""
152
+ response = APIResponse(
153
+ success=False,
154
+ error=error_message,
155
+ error_code=error_code
156
+ )
157
+ self._send_response(response, status_code)
158
+
159
+ def _send_cors_headers(self):
160
+ """发送CORS头"""
161
+ self.send_header('Access-Control-Allow-Origin', '*')
162
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
163
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
164
+
165
+ def _get_param(self, params: Dict, key: str, default: Any = None, required: bool = False):
166
+ """获取参数值"""
167
+ if key in params:
168
+ return params[key][0] if isinstance(params[key], list) else params[key]
169
+ elif required:
170
+ raise ValueError(f"缺少必需参数: {key}")
171
+ else:
172
+ return default
173
+
174
+ # API端点处理方法
175
+ def _handle_root(self):
176
+ """处理根路径"""
177
+ response = APIResponse(
178
+ success=True,
179
+ data={
180
+ "name": "Staran API Server",
181
+ "version": "1.0.10",
182
+ "description": "Staran日期处理REST API服务",
183
+ "endpoints": [
184
+ "/api/health",
185
+ "/api/date/create",
186
+ "/api/date/format",
187
+ "/api/date/calculate",
188
+ "/api/date/batch",
189
+ "/api/lunar/convert",
190
+ "/api/solar-terms",
191
+ "/api/timezone/convert",
192
+ "/api/expression/parse",
193
+ "/api/visualization/data",
194
+ "/api/visualization/create",
195
+ "/api/docs"
196
+ ]
197
+ }
198
+ )
199
+ self._send_response(response)
200
+
201
+ def _handle_health(self):
202
+ """健康检查"""
203
+ response = APIResponse(
204
+ success=True,
205
+ data={"status": "healthy", "timestamp": datetime.datetime.now().isoformat()}
206
+ )
207
+ self._send_response(response)
208
+
209
+ def _handle_date_create(self, params: Dict):
210
+ """创建日期"""
211
+ try:
212
+ date_str = self._get_param(params, 'date', required=True)
213
+ format_str = self._get_param(params, 'format')
214
+
215
+ if format_str:
216
+ date_obj = self.Date.from_string(date_str, format_str)
217
+ else:
218
+ date_obj = self.Date(date_str)
219
+
220
+ response = APIResponse(
221
+ success=True,
222
+ data={
223
+ "date": date_obj.format_iso(),
224
+ "year": date_obj.year,
225
+ "month": date_obj.month,
226
+ "day": date_obj.day,
227
+ "weekday": date_obj.weekday(),
228
+ "formatted": {
229
+ "iso": date_obj.format_iso(),
230
+ "chinese": date_obj.format_chinese(),
231
+ "readable": date_obj.format_readable()
232
+ }
233
+ }
234
+ )
235
+ self._send_response(response)
236
+
237
+ except Exception as e:
238
+ self._send_error_response(400, "DATE_CREATE_ERROR", str(e))
239
+
240
+ def _handle_date_format(self, params: Dict):
241
+ """格式化日期"""
242
+ try:
243
+ date_str = self._get_param(params, 'date', required=True)
244
+ format_type = self._get_param(params, 'format', 'iso')
245
+ language = self._get_param(params, 'language', 'zh_CN')
246
+
247
+ date_obj = self.Date(date_str)
248
+
249
+ formatted_result = {}
250
+
251
+ if format_type == 'iso':
252
+ formatted_result['result'] = date_obj.format_iso()
253
+ elif format_type == 'chinese':
254
+ formatted_result['result'] = date_obj.format_chinese()
255
+ elif format_type == 'readable':
256
+ formatted_result['result'] = date_obj.format_readable()
257
+ elif format_type == 'localized':
258
+ self.Date.set_language(language)
259
+ formatted_result['result'] = date_obj.format_localized()
260
+ elif format_type == 'all':
261
+ self.Date.set_language(language)
262
+ formatted_result = {
263
+ 'iso': date_obj.format_iso(),
264
+ 'chinese': date_obj.format_chinese(),
265
+ 'readable': date_obj.format_readable(),
266
+ 'localized': date_obj.format_localized(),
267
+ 'weekday': date_obj.format_weekday_localized(),
268
+ 'month': date_obj.format_month_localized()
269
+ }
270
+ else:
271
+ formatted_result['result'] = date_obj.format_custom(format_type)
272
+
273
+ response = APIResponse(success=True, data=formatted_result)
274
+ self._send_response(response)
275
+
276
+ except Exception as e:
277
+ self._send_error_response(400, "DATE_FORMAT_ERROR", str(e))
278
+
279
+ def _handle_date_calculate(self, params: Dict):
280
+ """日期计算"""
281
+ try:
282
+ date_str = self._get_param(params, 'date', required=True)
283
+ operation = self._get_param(params, 'operation', required=True)
284
+ value = int(self._get_param(params, 'value', 0))
285
+
286
+ date_obj = self.Date(date_str)
287
+
288
+ if operation == 'add_days':
289
+ result_date = date_obj.add_days(value)
290
+ elif operation == 'add_months':
291
+ result_date = date_obj.add_months(value)
292
+ elif operation == 'add_years':
293
+ result_date = date_obj.add_years(value)
294
+ elif operation == 'subtract_days':
295
+ result_date = date_obj.subtract_days(value)
296
+ elif operation == 'subtract_months':
297
+ result_date = date_obj.subtract_months(value)
298
+ elif operation == 'subtract_years':
299
+ result_date = date_obj.subtract_years(value)
300
+ else:
301
+ raise ValueError(f"不支持的操作: {operation}")
302
+
303
+ response = APIResponse(
304
+ success=True,
305
+ data={
306
+ "original_date": date_obj.format_iso(),
307
+ "operation": operation,
308
+ "value": value,
309
+ "result_date": result_date.format_iso(),
310
+ "difference_days": result_date.calculate_days_difference(date_obj)
311
+ }
312
+ )
313
+ self._send_response(response)
314
+
315
+ except Exception as e:
316
+ self._send_error_response(400, "DATE_CALCULATE_ERROR", str(e))
317
+
318
+ def _handle_date_batch(self, request_data: Dict):
319
+ """批量日期处理"""
320
+ try:
321
+ dates = request_data.get('dates', [])
322
+ operation = request_data.get('operation', 'format')
323
+ options = request_data.get('options', {})
324
+
325
+ results = []
326
+
327
+ for date_str in dates:
328
+ try:
329
+ date_obj = self.Date(date_str)
330
+
331
+ if operation == 'format':
332
+ format_type = options.get('format', 'iso')
333
+ if format_type == 'iso':
334
+ result = date_obj.format_iso()
335
+ elif format_type == 'chinese':
336
+ result = date_obj.format_chinese()
337
+ else:
338
+ result = date_obj.format_custom(format_type)
339
+ elif operation == 'add_days':
340
+ days = options.get('days', 0)
341
+ result = date_obj.add_days(days).format_iso()
342
+ elif operation == 'to_lunar':
343
+ lunar = date_obj.to_lunar()
344
+ result = {
345
+ 'year': lunar.year,
346
+ 'month': lunar.month,
347
+ 'day': lunar.day,
348
+ 'formatted': lunar.format_chinese()
349
+ }
350
+ else:
351
+ result = date_obj.format_iso()
352
+
353
+ results.append({
354
+ 'input': date_str,
355
+ 'result': result,
356
+ 'success': True
357
+ })
358
+
359
+ except Exception as e:
360
+ results.append({
361
+ 'input': date_str,
362
+ 'error': str(e),
363
+ 'success': False
364
+ })
365
+
366
+ response = APIResponse(
367
+ success=True,
368
+ data={
369
+ 'operation': operation,
370
+ 'total_count': len(dates),
371
+ 'success_count': sum(1 for r in results if r['success']),
372
+ 'results': results
373
+ }
374
+ )
375
+ self._send_response(response)
376
+
377
+ except Exception as e:
378
+ self._send_error_response(400, "BATCH_PROCESS_ERROR", str(e))
379
+
380
+ def _handle_lunar_convert(self, params: Dict):
381
+ """农历转换"""
382
+ try:
383
+ date_str = self._get_param(params, 'date', required=True)
384
+ direction = self._get_param(params, 'direction', 'solar_to_lunar')
385
+
386
+ if direction == 'solar_to_lunar':
387
+ date_obj = self.Date(date_str)
388
+ lunar = date_obj.to_lunar()
389
+ result = {
390
+ 'solar_date': date_obj.format_iso(),
391
+ 'lunar_year': lunar.year,
392
+ 'lunar_month': lunar.month,
393
+ 'lunar_day': lunar.day,
394
+ 'lunar_formatted': lunar.format_chinese(),
395
+ 'ganzhi_year': lunar.get_ganzhi_year(),
396
+ 'zodiac': lunar.get_zodiac()
397
+ }
398
+ elif direction == 'lunar_to_solar':
399
+ # 解析农历日期参数
400
+ year = int(self._get_param(params, 'year', required=True))
401
+ month = int(self._get_param(params, 'month', required=True))
402
+ day = int(self._get_param(params, 'day', required=True))
403
+ is_leap = self._get_param(params, 'is_leap', 'false').lower() == 'true'
404
+
405
+ date_obj = self.Date.from_lunar(year, month, day, is_leap)
406
+ result = {
407
+ 'lunar_date': f"{year}-{month:02d}-{day:02d}",
408
+ 'solar_date': date_obj.format_iso(),
409
+ 'solar_formatted': date_obj.format_chinese()
410
+ }
411
+ else:
412
+ raise ValueError(f"不支持的转换方向: {direction}")
413
+
414
+ response = APIResponse(success=True, data=result)
415
+ self._send_response(response)
416
+
417
+ except Exception as e:
418
+ self._send_error_response(400, "LUNAR_CONVERT_ERROR", str(e))
419
+
420
+ def _handle_solar_terms(self, params: Dict):
421
+ """二十四节气查询"""
422
+ try:
423
+ year = int(self._get_param(params, 'year', datetime.date.today().year))
424
+ term_name = self._get_param(params, 'term')
425
+
426
+ if term_name:
427
+ # 查询特定节气
428
+ solar_term = self.SolarTerms.find_solar_term_by_name(year, term_name)
429
+ if solar_term:
430
+ result = {
431
+ 'name': solar_term.name,
432
+ 'date': solar_term.date.strftime('%Y-%m-%d'),
433
+ 'season': solar_term.season,
434
+ 'description': solar_term.description,
435
+ 'climate_features': solar_term.climate_features,
436
+ 'traditional_activities': solar_term.traditional_activities,
437
+ 'agricultural_guidance': solar_term.agricultural_guidance
438
+ }
439
+ else:
440
+ raise ValueError(f"未找到节气: {term_name}")
441
+ else:
442
+ # 查询全年节气
443
+ all_terms = self.SolarTerms.get_all_solar_terms(year)
444
+ result = []
445
+ for term in all_terms:
446
+ result.append({
447
+ 'name': term.name,
448
+ 'date': term.date.strftime('%Y-%m-%d'),
449
+ 'season': term.season,
450
+ 'description': term.description
451
+ })
452
+
453
+ response = APIResponse(success=True, data=result)
454
+ self._send_response(response)
455
+
456
+ except Exception as e:
457
+ self._send_error_response(400, "SOLAR_TERMS_ERROR", str(e))
458
+
459
+ def _handle_timezone_convert(self, params: Dict):
460
+ """时区转换"""
461
+ try:
462
+ date_str = self._get_param(params, 'date', required=True)
463
+ time_str = self._get_param(params, 'time', '00:00:00')
464
+ from_tz = self._get_param(params, 'from_tz', required=True)
465
+ to_tz = self._get_param(params, 'to_tz', required=True)
466
+
467
+ # 解析日期时间
468
+ date_part = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
469
+ time_part = datetime.datetime.strptime(time_str, '%H:%M:%S').time()
470
+ dt = datetime.datetime.combine(date_part, time_part)
471
+
472
+ # 时区转换
473
+ converted_dt = self.Timezone.convert_timezone(dt, from_tz, to_tz)
474
+
475
+ result = {
476
+ 'original_datetime': dt.strftime('%Y-%m-%d %H:%M:%S'),
477
+ 'original_timezone': from_tz,
478
+ 'converted_datetime': converted_dt.strftime('%Y-%m-%d %H:%M:%S'),
479
+ 'converted_timezone': to_tz,
480
+ 'timezone_info': {
481
+ 'from': self.Timezone.get_timezone_display_info(from_tz),
482
+ 'to': self.Timezone.get_timezone_display_info(to_tz)
483
+ }
484
+ }
485
+
486
+ response = APIResponse(success=True, data=result)
487
+ self._send_response(response)
488
+
489
+ except Exception as e:
490
+ self._send_error_response(400, "TIMEZONE_CONVERT_ERROR", str(e))
491
+
492
+ def _handle_expression_parse(self, params: Dict):
493
+ """日期表达式解析"""
494
+ try:
495
+ expression = self._get_param(params, 'expression', required=True)
496
+
497
+ parser = self.DateExpressionParser()
498
+ parse_result = parser.parse(expression)
499
+
500
+ if parse_result.success:
501
+ result = {
502
+ 'success': True,
503
+ 'parsed_date': parse_result.date.strftime('%Y-%m-%d'),
504
+ 'confidence': parse_result.confidence,
505
+ 'matched_pattern': parse_result.matched_pattern,
506
+ 'extracted_components': parse_result.extracted_components
507
+ }
508
+ else:
509
+ result = {
510
+ 'success': False,
511
+ 'message': '无法解析表达式'
512
+ }
513
+
514
+ response = APIResponse(success=True, data=result)
515
+ self._send_response(response)
516
+
517
+ except Exception as e:
518
+ self._send_error_response(400, "EXPRESSION_PARSE_ERROR", str(e))
519
+
520
+ def _handle_visualization_data(self, params: Dict):
521
+ """获取可视化数据"""
522
+ try:
523
+ chart_type = self._get_param(params, 'type', required=True)
524
+ library = self._get_param(params, 'library', 'echarts')
525
+
526
+ viz = self.DateVisualization()
527
+
528
+ if chart_type == 'calendar_heatmap':
529
+ year = int(self._get_param(params, 'year', datetime.date.today().year))
530
+ # 生成示例数据
531
+ import random
532
+ date_values = {}
533
+ start_date = datetime.date(year, 1, 1)
534
+ for i in range(365):
535
+ date = start_date + datetime.timedelta(days=i)
536
+ date_values[date] = random.randint(0, 100)
537
+
538
+ chart_data = viz.create_calendar_heatmap(date_values, year, library)
539
+
540
+ elif chart_type == 'solar_terms':
541
+ year = int(self._get_param(params, 'year', datetime.date.today().year))
542
+ chart_data = viz.create_solar_terms_chart(year, library)
543
+
544
+ else:
545
+ raise ValueError(f"不支持的图表类型: {chart_type}")
546
+
547
+ response = APIResponse(success=True, data=asdict(chart_data))
548
+ self._send_response(response)
549
+
550
+ except Exception as e:
551
+ self._send_error_response(400, "VISUALIZATION_DATA_ERROR", str(e))
552
+
553
+ def _handle_visualization_create(self, request_data: Dict):
554
+ """创建可视化图表"""
555
+ try:
556
+ chart_type = request_data.get('type', 'timeline')
557
+ library = request_data.get('library', 'echarts')
558
+ data = request_data.get('data', [])
559
+
560
+ viz = self.DateVisualization()
561
+
562
+ if chart_type == 'timeline':
563
+ dates = [datetime.datetime.strptime(item['date'], '%Y-%m-%d').date() for item in data]
564
+ events = [item['event'] for item in data]
565
+ chart_data = viz.create_timeline_data(dates, events, library)
566
+
567
+ elif chart_type == 'time_series':
568
+ from .visualization import TimeSeriesPoint
569
+ time_series = []
570
+ for item in data:
571
+ date = datetime.datetime.strptime(item['date'], '%Y-%m-%d').date()
572
+ value = item['value']
573
+ point = TimeSeriesPoint(date, value, item.get('label'), item.get('category'))
574
+ time_series.append(point)
575
+ chart_data = viz.create_time_series_chart(time_series, library)
576
+
577
+ else:
578
+ raise ValueError(f"不支持的图表类型: {chart_type}")
579
+
580
+ response = APIResponse(success=True, data=asdict(chart_data))
581
+ self._send_response(response)
582
+
583
+ except Exception as e:
584
+ self._send_error_response(400, "VISUALIZATION_CREATE_ERROR", str(e))
585
+
586
+ def _handle_api_docs(self):
587
+ """API文档"""
588
+ docs = {
589
+ "title": "Staran Date API Documentation",
590
+ "version": "1.0.10",
591
+ "description": "完整的日期处理REST API服务",
592
+ "endpoints": {
593
+ "GET /api/health": {
594
+ "description": "健康检查",
595
+ "parameters": {},
596
+ "response": "健康状态信息"
597
+ },
598
+ "GET /api/date/create": {
599
+ "description": "创建日期对象",
600
+ "parameters": {
601
+ "date": "日期字符串 (必需)",
602
+ "format": "日期格式 (可选)"
603
+ },
604
+ "response": "日期对象信息"
605
+ },
606
+ "GET /api/date/format": {
607
+ "description": "格式化日期",
608
+ "parameters": {
609
+ "date": "日期字符串 (必需)",
610
+ "format": "格式类型 (iso|chinese|readable|localized|all)",
611
+ "language": "语言代码 (zh_CN|zh_TW|ja_JP|en_US)"
612
+ },
613
+ "response": "格式化后的日期"
614
+ },
615
+ "GET /api/date/calculate": {
616
+ "description": "日期计算",
617
+ "parameters": {
618
+ "date": "日期字符串 (必需)",
619
+ "operation": "操作类型 (add_days|add_months|add_years|subtract_days|subtract_months|subtract_years)",
620
+ "value": "计算值"
621
+ },
622
+ "response": "计算结果"
623
+ },
624
+ "POST /api/date/batch": {
625
+ "description": "批量日期处理",
626
+ "body": {
627
+ "dates": ["日期字符串数组"],
628
+ "operation": "操作类型",
629
+ "options": "操作选项"
630
+ },
631
+ "response": "批量处理结果"
632
+ },
633
+ "GET /api/lunar/convert": {
634
+ "description": "农历转换",
635
+ "parameters": {
636
+ "date": "日期字符串",
637
+ "direction": "转换方向 (solar_to_lunar|lunar_to_solar)",
638
+ "year": "农历年 (lunar_to_solar时必需)",
639
+ "month": "农历月 (lunar_to_solar时必需)",
640
+ "day": "农历日 (lunar_to_solar时必需)",
641
+ "is_leap": "是否闰月 (lunar_to_solar时可选)"
642
+ },
643
+ "response": "转换结果"
644
+ },
645
+ "GET /api/solar-terms": {
646
+ "description": "二十四节气查询",
647
+ "parameters": {
648
+ "year": "年份",
649
+ "term": "节气名称 (可选,不提供则返回全年)"
650
+ },
651
+ "response": "节气信息"
652
+ },
653
+ "GET /api/timezone/convert": {
654
+ "description": "时区转换",
655
+ "parameters": {
656
+ "date": "日期字符串 (必需)",
657
+ "time": "时间字符串 (HH:MM:SS)",
658
+ "from_tz": "源时区 (必需)",
659
+ "to_tz": "目标时区 (必需)"
660
+ },
661
+ "response": "时区转换结果"
662
+ },
663
+ "GET /api/expression/parse": {
664
+ "description": "日期表达式解析",
665
+ "parameters": {
666
+ "expression": "日期表达式 (必需)"
667
+ },
668
+ "response": "解析结果"
669
+ },
670
+ "GET /api/visualization/data": {
671
+ "description": "获取可视化数据",
672
+ "parameters": {
673
+ "type": "图表类型 (必需)",
674
+ "library": "图表库 (echarts|matplotlib|plotly|chartjs|highcharts)",
675
+ "year": "年份 (某些图表类型需要)"
676
+ },
677
+ "response": "图表数据"
678
+ },
679
+ "POST /api/visualization/create": {
680
+ "description": "创建可视化图表",
681
+ "body": {
682
+ "type": "图表类型",
683
+ "library": "图表库",
684
+ "data": "图表数据"
685
+ },
686
+ "response": "图表配置"
687
+ }
688
+ },
689
+ "supported_timezones": self.Timezone.list_timezones() if hasattr(self, 'Timezone') else [],
690
+ "supported_languages": ["zh_CN", "zh_TW", "ja_JP", "en_US"],
691
+ "examples": {
692
+ "create_date": "/api/date/create?date=2025-07-29",
693
+ "format_chinese": "/api/date/format?date=2025-07-29&format=chinese",
694
+ "solar_to_lunar": "/api/lunar/convert?date=2025-07-29&direction=solar_to_lunar",
695
+ "parse_expression": "/api/expression/parse?expression=明天",
696
+ "solar_terms": "/api/solar-terms?year=2025"
697
+ }
698
+ }
699
+
700
+ response = APIResponse(success=True, data=docs)
701
+ self._send_response(response)
702
+
703
+ class StaranAPIServer:
704
+ """Staran API 服务器"""
705
+
706
+ def __init__(self, host: str = 'localhost', port: int = 8000):
707
+ self.host = host
708
+ self.port = port
709
+ self.server = None
710
+ self.server_thread = None
711
+
712
+ def start(self, background: bool = False):
713
+ """启动服务器"""
714
+ try:
715
+ self.server = HTTPServer((self.host, self.port), StaranAPIHandler)
716
+ logger.info(f"Staran API服务器启动在 http://{self.host}:{self.port}")
717
+
718
+ if background:
719
+ self.server_thread = threading.Thread(target=self.server.serve_forever)
720
+ self.server_thread.daemon = True
721
+ self.server_thread.start()
722
+ logger.info("API服务器在后台运行")
723
+ else:
724
+ logger.info("按 Ctrl+C 停止服务器")
725
+ self.server.serve_forever()
726
+
727
+ except KeyboardInterrupt:
728
+ logger.info("收到停止信号")
729
+ except Exception as e:
730
+ logger.error(f"启动服务器时出错: {e}")
731
+ finally:
732
+ if self.server:
733
+ self.server.server_close()
734
+
735
+ def stop(self):
736
+ """停止服务器"""
737
+ if self.server:
738
+ self.server.shutdown()
739
+ self.server.server_close()
740
+ logger.info("API服务器已停止")
741
+
742
+ if self.server_thread and self.server_thread.is_alive():
743
+ self.server_thread.join(timeout=5)
744
+
745
+ # 便捷函数
746
+ def start_api_server(host: str = 'localhost', port: int = 8000, background: bool = False):
747
+ """启动API服务器(便捷函数)"""
748
+ server = StaranAPIServer(host, port)
749
+ server.start(background)
750
+ return server
751
+
752
+ def create_api_response(success: bool, data: Any = None, error: str = None, error_code: str = None) -> APIResponse:
753
+ """创建API响应(便捷函数)"""
754
+ return APIResponse(success=success, data=data, error=error, error_code=error_code)