comci 0.1.0__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.
- comci-0.1.0/PKG-INFO +90 -0
- comci-0.1.0/README.md +82 -0
- comci-0.1.0/comci/__init__.py +16 -0
- comci-0.1.0/comci/client.py +69 -0
- comci-0.1.0/comci/config.py +34 -0
- comci-0.1.0/comci/school_search.py +58 -0
- comci-0.1.0/comci/timetable.py +251 -0
- comci-0.1.0/comci.egg-info/PKG-INFO +90 -0
- comci-0.1.0/comci.egg-info/SOURCES.txt +12 -0
- comci-0.1.0/comci.egg-info/dependency_links.txt +1 -0
- comci-0.1.0/comci.egg-info/requires.txt +1 -0
- comci-0.1.0/comci.egg-info/top_level.txt +1 -0
- comci-0.1.0/pyproject.toml +15 -0
- comci-0.1.0/setup.cfg +4 -0
comci-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: comci
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Comci.net 시간표 API 비공식 클라이언트
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: requests>=2.28.0
|
|
8
|
+
|
|
9
|
+
# Comci.net 시간표 API 클라이언트
|
|
10
|
+
|
|
11
|
+
컴시간(comci.net) 비공식 API를 사용하여 학교 검색 및 시간표 조회를 할 수 있는 Python 모듈입니다.
|
|
12
|
+
|
|
13
|
+
## 설치
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -r requirements.txt
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 사용법
|
|
20
|
+
|
|
21
|
+
### 1. 학교 검색 (지역, 학교명, 학교코드)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from comci import search_schools
|
|
25
|
+
|
|
26
|
+
# "신송" 검색
|
|
27
|
+
schools = search_schools("신송")
|
|
28
|
+
for s in schools:
|
|
29
|
+
print(s["region"], s["school_name"], s["school_code"])
|
|
30
|
+
# 인천 신송중학교 49654
|
|
31
|
+
# 인천 신송고등학교 51825
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. 특정 학년/반 시간표 조회
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from comci import get_timetable
|
|
38
|
+
|
|
39
|
+
# 1학년 1반 시간표 (정리.md 반환 구조)
|
|
40
|
+
timetable = get_timetable(49654, grade=1, class_num=1)
|
|
41
|
+
# {"월": [...], "화": [...], "수": [...], "목": [...], "금": [...]}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. 전체 학년/반 시간표 조회
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# 학년, 반 미지정 시 전체
|
|
48
|
+
timetable = get_timetable(49654)
|
|
49
|
+
# {"1학년 1반": {...}, "1학년 2반": {...}, ...}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 4. 검색 후 바로 시간표 조회
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from comci import search_and_get_timetable
|
|
56
|
+
|
|
57
|
+
timetable = search_and_get_timetable("신송", school_index=0, grade=1, class_num=1)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 프로젝트 구조
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
comci/
|
|
64
|
+
├── __init__.py # 패키지 진입점
|
|
65
|
+
├── config.py # API URL, 상수
|
|
66
|
+
├── school_search.py # 학교 검색
|
|
67
|
+
├── timetable.py # 시간표 API 및 파싱
|
|
68
|
+
└── client.py # 통합 클라이언트
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 예시 실행
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python example_usage.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 시간표 데이터 구조
|
|
78
|
+
|
|
79
|
+
각 요일별 리스트는 교시(1~8) 순서이며, 각 항목은:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
{
|
|
83
|
+
"period": 1, # 교시
|
|
84
|
+
"time": "08:40", # 시간
|
|
85
|
+
"subject": "국어", # 과목
|
|
86
|
+
"teacher": "김선생", # 교사
|
|
87
|
+
"room": "101", # 강의실
|
|
88
|
+
"changed": False # 변경 수업 여부
|
|
89
|
+
}
|
|
90
|
+
```
|
comci-0.1.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Comci.net 시간표 API 클라이언트
|
|
2
|
+
|
|
3
|
+
컴시간(comci.net) 비공식 API를 사용하여 학교 검색 및 시간표 조회를 할 수 있는 Python 모듈입니다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -r requirements.txt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 사용법
|
|
12
|
+
|
|
13
|
+
### 1. 학교 검색 (지역, 학교명, 학교코드)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from comci import search_schools
|
|
17
|
+
|
|
18
|
+
# "신송" 검색
|
|
19
|
+
schools = search_schools("신송")
|
|
20
|
+
for s in schools:
|
|
21
|
+
print(s["region"], s["school_name"], s["school_code"])
|
|
22
|
+
# 인천 신송중학교 49654
|
|
23
|
+
# 인천 신송고등학교 51825
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. 특정 학년/반 시간표 조회
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from comci import get_timetable
|
|
30
|
+
|
|
31
|
+
# 1학년 1반 시간표 (정리.md 반환 구조)
|
|
32
|
+
timetable = get_timetable(49654, grade=1, class_num=1)
|
|
33
|
+
# {"월": [...], "화": [...], "수": [...], "목": [...], "금": [...]}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. 전체 학년/반 시간표 조회
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# 학년, 반 미지정 시 전체
|
|
40
|
+
timetable = get_timetable(49654)
|
|
41
|
+
# {"1학년 1반": {...}, "1학년 2반": {...}, ...}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. 검색 후 바로 시간표 조회
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from comci import search_and_get_timetable
|
|
48
|
+
|
|
49
|
+
timetable = search_and_get_timetable("신송", school_index=0, grade=1, class_num=1)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 프로젝트 구조
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
comci/
|
|
56
|
+
├── __init__.py # 패키지 진입점
|
|
57
|
+
├── config.py # API URL, 상수
|
|
58
|
+
├── school_search.py # 학교 검색
|
|
59
|
+
├── timetable.py # 시간표 API 및 파싱
|
|
60
|
+
└── client.py # 통합 클라이언트
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 예시 실행
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python example_usage.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 시간표 데이터 구조
|
|
70
|
+
|
|
71
|
+
각 요일별 리스트는 교시(1~8) 순서이며, 각 항목은:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
{
|
|
75
|
+
"period": 1, # 교시
|
|
76
|
+
"time": "08:40", # 시간
|
|
77
|
+
"subject": "국어", # 과목
|
|
78
|
+
"teacher": "김선생", # 교사
|
|
79
|
+
"room": "101", # 강의실
|
|
80
|
+
"changed": False # 변경 수업 여부
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comci.net 시간표 API 클라이언트
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import get_timetable, search_and_get_schools, search_and_get_timetable
|
|
6
|
+
from .school_search import search_schools
|
|
7
|
+
from .timetable import fetch_timetable_raw, parse_timetable
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"search_schools",
|
|
11
|
+
"search_and_get_schools",
|
|
12
|
+
"fetch_timetable_raw",
|
|
13
|
+
"parse_timetable",
|
|
14
|
+
"get_timetable",
|
|
15
|
+
"search_and_get_timetable",
|
|
16
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comci.net API 통합 클라이언트
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .school_search import search_schools
|
|
8
|
+
from .timetable import fetch_timetable_raw, parse_timetable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def search_and_get_schools(query: str) -> List[Dict[str, Any]]:
|
|
12
|
+
"""
|
|
13
|
+
검색어로 학교 검색
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
[{"region": "인천", "school_name": "신송중학교", "school_code": 49654}, ...]
|
|
17
|
+
"""
|
|
18
|
+
return search_schools(query)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_timetable(
|
|
22
|
+
school_code: int,
|
|
23
|
+
grade: Optional[int] = None,
|
|
24
|
+
class_num: Optional[int] = None,
|
|
25
|
+
date_index: int = 1,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
학교 시간표 조회 (정리.md 기준)
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
school_code: 학교코드
|
|
32
|
+
grade: 학년 (1-based). None이면 전체
|
|
33
|
+
class_num: 반 (1-based). None이면 전체
|
|
34
|
+
date_index: 날짜 인덱스 (r)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
- grade, class_num 지정 시: {"월": [...], "화": [...], "수": [...], "목": [...], "금": [...]}
|
|
38
|
+
- 미지정 시: {"1학년 1반": {"월": [...], ...}, "1학년 2반": {...}, ...}
|
|
39
|
+
"""
|
|
40
|
+
raw = fetch_timetable_raw(school_code, date_index)
|
|
41
|
+
parsed = parse_timetable(raw, grade=grade, class_num=class_num)
|
|
42
|
+
if grade is not None and class_num is not None and parsed:
|
|
43
|
+
return next(iter(parsed.values()))
|
|
44
|
+
return parsed
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def search_and_get_timetable(
|
|
48
|
+
query: str,
|
|
49
|
+
school_index: int = 0,
|
|
50
|
+
grade: Optional[int] = None,
|
|
51
|
+
class_num: Optional[int] = None,
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""
|
|
54
|
+
검색 → 학교 선택 → 시간표 조회를 한 번에 수행
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
query: 검색어
|
|
58
|
+
school_index: 검색 결과 중 선택할 학교 인덱스 (0=첫 번째)
|
|
59
|
+
grade: 학년 (None=전체)
|
|
60
|
+
class_num: 반 (None=전체)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
시간표 데이터
|
|
64
|
+
"""
|
|
65
|
+
schools = search_schools(query)
|
|
66
|
+
if not schools:
|
|
67
|
+
return {}
|
|
68
|
+
school = schools[school_index]
|
|
69
|
+
return get_timetable(school["school_code"], grade=grade, class_num=class_num)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comci.net API 설정
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# API 기본 URL
|
|
6
|
+
BASE_URL = "http://comci.net:4082/36179"
|
|
7
|
+
|
|
8
|
+
# 학교 검색 API 파라미터 prefix
|
|
9
|
+
SEARCH_PREFIX = "17384l"
|
|
10
|
+
|
|
11
|
+
# 시간표 API 고정 키
|
|
12
|
+
TIMETABLE_KEY = "73629"
|
|
13
|
+
TIMETABLE_CACHE = "0"
|
|
14
|
+
|
|
15
|
+
# 기본 날짜 인덱스 (r 파라미터)
|
|
16
|
+
DEFAULT_DATE_INDEX = 1
|
|
17
|
+
|
|
18
|
+
# 요청 헤더 (정리.md 기준)
|
|
19
|
+
DEFAULT_HEADERS = {
|
|
20
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
21
|
+
"Accept": "application/json,text/plain,*/*",
|
|
22
|
+
"Accept-Language": "ko-KR,ko;q=0.9",
|
|
23
|
+
"Referer": "http://comci.net:4082/st",
|
|
24
|
+
"x-requested-with": "XMLHttpRequest",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# 인코딩
|
|
28
|
+
SEARCH_ENCODING = "euc-kr"
|
|
29
|
+
|
|
30
|
+
# 요일 매핑
|
|
31
|
+
DAYS_OF_WEEK = ["월", "화", "수", "목", "금"]
|
|
32
|
+
|
|
33
|
+
# 빈 수업 판단 기준
|
|
34
|
+
EMPTY_CLASS_THRESHOLD = 100
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
학교 검색 API 모듈
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .config import BASE_URL, SEARCH_PREFIX, SEARCH_ENCODING, DEFAULT_HEADERS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def encode_search_query(query: str) -> str:
|
|
15
|
+
"""검색어를 EUC-KR 인코딩 후 퍼센트 인코딩"""
|
|
16
|
+
return urllib.parse.quote(query, encoding=SEARCH_ENCODING, safe="")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def search_schools(query: str) -> List[Dict[str, Any]]:
|
|
20
|
+
"""
|
|
21
|
+
학교 검색 API 호출
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
query: 검색어 (예: "신송", "신")
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
검색 결과 리스트. 각 항목: {region, school_name, school_code}
|
|
28
|
+
알림 메시지(없으면 추가 검색하세요)는 제외됨
|
|
29
|
+
"""
|
|
30
|
+
encoded = encode_search_query(query)
|
|
31
|
+
url = f"{BASE_URL}?{SEARCH_PREFIX}{encoded}"
|
|
32
|
+
|
|
33
|
+
response = requests.get(url, headers=DEFAULT_HEADERS, timeout=10)
|
|
34
|
+
response.raise_for_status()
|
|
35
|
+
# API 응답 인코딩 (JSON은 UTF-8)
|
|
36
|
+
if response.encoding in (None, "ISO-8859-1", "ascii"):
|
|
37
|
+
response.encoding = "utf-8"
|
|
38
|
+
|
|
39
|
+
# API가 여러 JSON 객체를 연속 반환할 수 있음 - 첫 번째만 파싱
|
|
40
|
+
data, _ = json.JSONDecoder().raw_decode(response.text)
|
|
41
|
+
raw_results = data.get("학교검색", [])
|
|
42
|
+
if not raw_results and data:
|
|
43
|
+
# 단일 키인 경우 해당 값 사용
|
|
44
|
+
vals = list(data.values())
|
|
45
|
+
if vals and isinstance(vals[0], list):
|
|
46
|
+
raw_results = vals[0]
|
|
47
|
+
|
|
48
|
+
results = []
|
|
49
|
+
for item in raw_results:
|
|
50
|
+
# [0, "알림", "없으면 추가 검색하세요", 0] 형태의 알림 제외
|
|
51
|
+
if len(item) >= 4 and item[1] != "알림" and item[3] != 0:
|
|
52
|
+
results.append({
|
|
53
|
+
"region": item[1],
|
|
54
|
+
"school_name": item[2],
|
|
55
|
+
"school_code": item[3],
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return results
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
시간표 API 및 파싱 모듈
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .config import (
|
|
12
|
+
BASE_URL,
|
|
13
|
+
TIMETABLE_KEY,
|
|
14
|
+
TIMETABLE_CACHE,
|
|
15
|
+
DEFAULT_DATE_INDEX,
|
|
16
|
+
DEFAULT_HEADERS,
|
|
17
|
+
DAYS_OF_WEEK,
|
|
18
|
+
EMPTY_CLASS_THRESHOLD,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_timetable_param(school_code: int, date_index: int = DEFAULT_DATE_INDEX) -> str:
|
|
23
|
+
"""시간표 API용 Base64 파라미터 생성"""
|
|
24
|
+
raw = f"{TIMETABLE_KEY}_{school_code}_{TIMETABLE_CACHE}_{date_index}"
|
|
25
|
+
return base64.b64encode(raw.encode("utf-8")).decode("utf-8")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def fetch_timetable_raw(school_code: int, date_index: int = DEFAULT_DATE_INDEX) -> Dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
학교 시간표 원본 JSON 조회
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
school_code: 학교코드 (검색 API에서 획득)
|
|
34
|
+
date_index: 날짜 인덱스 (r 파라미터)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
API 응답 JSON 전체
|
|
38
|
+
"""
|
|
39
|
+
param = build_timetable_param(school_code, date_index)
|
|
40
|
+
url = f"{BASE_URL}?{param}"
|
|
41
|
+
|
|
42
|
+
response = requests.get(url, headers=DEFAULT_HEADERS, timeout=10)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
if response.encoding in (None, "ISO-8859-1", "ascii"):
|
|
45
|
+
response.encoding = "utf-8"
|
|
46
|
+
|
|
47
|
+
# API가 여러 JSON 객체를 연속 반환할 수 있음 - 자료481 포함된 객체 찾기
|
|
48
|
+
text = response.text.strip()
|
|
49
|
+
decoder = json.JSONDecoder()
|
|
50
|
+
idx = 0
|
|
51
|
+
while idx < len(text):
|
|
52
|
+
try:
|
|
53
|
+
data, end = decoder.raw_decode(text[idx:])
|
|
54
|
+
if isinstance(data, dict) and (
|
|
55
|
+
"자료481" in data or any("481" in str(k) for k in data.keys())
|
|
56
|
+
):
|
|
57
|
+
return data
|
|
58
|
+
idx += end
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
break
|
|
61
|
+
data, _ = json.JSONDecoder().raw_decode(text)
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_code(data: Any, grade: int, class_num: int, day: int, period: int) -> int:
|
|
66
|
+
"""
|
|
67
|
+
자료481/자료147에서 코드 추출
|
|
68
|
+
실제 구조: [numGrades, grade0, grade1, ...], grade_i=[numClasses, class0, ...],
|
|
69
|
+
class_j=[5, day0, day1, ...], day_k=[numPeriods, code1, code2, ...]
|
|
70
|
+
"""
|
|
71
|
+
if not data or not isinstance(data, list):
|
|
72
|
+
return 0
|
|
73
|
+
# grade: 0-based, 인덱스 0은 numGrades이므로 grade+1
|
|
74
|
+
if grade + 1 >= len(data):
|
|
75
|
+
return 0
|
|
76
|
+
grade_data = data[grade + 1]
|
|
77
|
+
if not isinstance(grade_data, list) or class_num + 1 >= len(grade_data):
|
|
78
|
+
return 0
|
|
79
|
+
class_data = grade_data[class_num + 1]
|
|
80
|
+
if not isinstance(class_data, list) or day >= len(class_data):
|
|
81
|
+
return 0
|
|
82
|
+
day_data = class_data[day] # day 1-based (1=월) -> 인덱스 1
|
|
83
|
+
if not isinstance(day_data, list) or period >= len(day_data):
|
|
84
|
+
return 0
|
|
85
|
+
# day_data[0] = num periods, 실제 코드는 [1]부터 (period 1-based)
|
|
86
|
+
val = day_data[period] if period < len(day_data) else 0
|
|
87
|
+
return int(val) if val else 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_room(data: Any, grade: int, class_num: int, day: int, period: int) -> str:
|
|
91
|
+
"""자료245에서 강의실 문자열 추출 (자료481과 동일 구조)"""
|
|
92
|
+
if not data or not isinstance(data, list):
|
|
93
|
+
return ""
|
|
94
|
+
if grade + 1 >= len(data):
|
|
95
|
+
return ""
|
|
96
|
+
grade_data = data[grade + 1]
|
|
97
|
+
if not isinstance(grade_data, list) or class_num + 1 >= len(grade_data):
|
|
98
|
+
return ""
|
|
99
|
+
class_data = grade_data[class_num + 1]
|
|
100
|
+
if not isinstance(class_data, list) or day >= len(class_data):
|
|
101
|
+
return ""
|
|
102
|
+
day_data = class_data[day] # day 1-based
|
|
103
|
+
if not isinstance(day_data, list) or period >= len(day_data):
|
|
104
|
+
return ""
|
|
105
|
+
val = day_data[period] # period 1-based
|
|
106
|
+
return str(val) if val else ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_single_class(
|
|
110
|
+
data: Dict[str, Any],
|
|
111
|
+
grade: int,
|
|
112
|
+
class_num: int,
|
|
113
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
114
|
+
"""
|
|
115
|
+
특정 학년/반의 시간표 파싱
|
|
116
|
+
grade, class_num은 0-based 인덱스
|
|
117
|
+
"""
|
|
118
|
+
split_val = data.get("분리", 100)
|
|
119
|
+
period_times = data.get("일과시간", [])
|
|
120
|
+
room_data = _get_data_key(data, "245")
|
|
121
|
+
base_data = _get_data_key(data, "481")
|
|
122
|
+
changed_data = _get_data_key(data, "147")
|
|
123
|
+
subject_data = _get_data_key(data, "492")
|
|
124
|
+
teacher_data = _get_data_key(data, "446")
|
|
125
|
+
|
|
126
|
+
timetable_by_day: Dict[str, List[Dict[str, Any]]] = {
|
|
127
|
+
day: [] for day in DAYS_OF_WEEK
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for period in range(1, 9): # 1~8교시
|
|
131
|
+
row = []
|
|
132
|
+
for day in range(1, 6): # 1~5 (월~금)
|
|
133
|
+
base = _get_code(base_data, grade, class_num, day, period)
|
|
134
|
+
changed = _get_code(changed_data, grade, class_num, day, period)
|
|
135
|
+
code = changed if changed > 0 else base
|
|
136
|
+
|
|
137
|
+
if not code or code <= EMPTY_CLASS_THRESHOLD:
|
|
138
|
+
row.append(None)
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# 정리.md: 분리=100 → teacher=floor(code/분리), subject=code%분리
|
|
142
|
+
# 분리=1000 → 공식 반대 (mTh/mSb 로직)
|
|
143
|
+
if split_val == 100:
|
|
144
|
+
teacher_idx = code // split_val
|
|
145
|
+
subject_idx = code % split_val
|
|
146
|
+
else:
|
|
147
|
+
teacher_idx = code % split_val
|
|
148
|
+
subject_idx = code // split_val
|
|
149
|
+
|
|
150
|
+
subject = subject_data[subject_idx] if subject_idx < len(subject_data) else "미정"
|
|
151
|
+
teacher = teacher_data[teacher_idx] if teacher_idx < len(teacher_data) else "미정"
|
|
152
|
+
|
|
153
|
+
room_raw = _get_room(room_data, grade, class_num, day, period)
|
|
154
|
+
parts = str(room_raw).split("_", 1)
|
|
155
|
+
room = parts[1] if len(parts) > 1 else (parts[0] if parts else "")
|
|
156
|
+
|
|
157
|
+
time_str = period_times[period - 1] if period <= len(period_times) else ""
|
|
158
|
+
|
|
159
|
+
row.append({
|
|
160
|
+
"period": period,
|
|
161
|
+
"time": time_str,
|
|
162
|
+
"day": day,
|
|
163
|
+
"subject": subject,
|
|
164
|
+
"teacher": teacher,
|
|
165
|
+
"room": room,
|
|
166
|
+
"changed": base != changed,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
for idx, day_name in enumerate(DAYS_OF_WEEK):
|
|
170
|
+
timetable_by_day[day_name].append(row[idx])
|
|
171
|
+
|
|
172
|
+
return timetable_by_day
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _get_nested(data: List, *indices: int):
|
|
176
|
+
"""중첩 리스트/딕셔너리 안전 접근"""
|
|
177
|
+
obj = data
|
|
178
|
+
for idx in indices:
|
|
179
|
+
if obj is None or (isinstance(obj, list) and idx >= len(obj)):
|
|
180
|
+
return 0
|
|
181
|
+
if isinstance(obj, list):
|
|
182
|
+
obj = obj[idx]
|
|
183
|
+
elif isinstance(obj, dict):
|
|
184
|
+
obj = obj.get(idx, 0)
|
|
185
|
+
return obj if obj is not None else 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _get_data_key(data: Dict, suffix: str):
|
|
189
|
+
"""키 인코딩 이슈 대응 - suffix로 키 찾기"""
|
|
190
|
+
key = "자료" + suffix
|
|
191
|
+
if key in data:
|
|
192
|
+
return data[key]
|
|
193
|
+
for k in data.keys():
|
|
194
|
+
if suffix in str(k):
|
|
195
|
+
return data[k]
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_timetable(
|
|
200
|
+
data: Dict[str, Any],
|
|
201
|
+
grade: Optional[int] = None,
|
|
202
|
+
class_num: Optional[int] = None,
|
|
203
|
+
) -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
시간표 JSON 파싱
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
data: fetch_timetable_raw() 반환값
|
|
209
|
+
grade: 학년 (1-based, 1=1학년). None이면 전체
|
|
210
|
+
class_num: 반 (1-based, 1=1반). None이면 전체
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
- grade, class_num 둘 다 지정: 해당 학년/반만
|
|
214
|
+
{"1학년 1반": {"월": [...], "화": [...], ...}}
|
|
215
|
+
- 둘 다 None: 전체 학년/반
|
|
216
|
+
{"1학년 1반": {...}, "1학년 2반": {...}, ...}
|
|
217
|
+
"""
|
|
218
|
+
base_data = _get_data_key(data, "481")
|
|
219
|
+
if not base_data or not isinstance(base_data, list):
|
|
220
|
+
return {}
|
|
221
|
+
|
|
222
|
+
num_grades = base_data[0] if isinstance(base_data[0], (int, float)) else len(base_data) - 1
|
|
223
|
+
result = {}
|
|
224
|
+
|
|
225
|
+
if grade is not None and class_num is not None:
|
|
226
|
+
g_idx = grade - 1
|
|
227
|
+
c_idx = class_num - 1
|
|
228
|
+
if g_idx < num_grades and g_idx + 1 < len(base_data):
|
|
229
|
+
grade_data = base_data[g_idx + 1]
|
|
230
|
+
num_classes = grade_data[0] if isinstance(grade_data, list) and grade_data else 0
|
|
231
|
+
if c_idx < num_classes:
|
|
232
|
+
key = f"{grade}학년 {class_num}반"
|
|
233
|
+
result[key] = _parse_single_class(data, g_idx, c_idx)
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
# 전체 학년/반
|
|
237
|
+
for g_idx in range(num_grades):
|
|
238
|
+
if g_idx + 1 >= len(base_data):
|
|
239
|
+
continue
|
|
240
|
+
grade_data = base_data[g_idx + 1]
|
|
241
|
+
if not isinstance(grade_data, list):
|
|
242
|
+
continue
|
|
243
|
+
num_classes = grade_data[0] if isinstance(grade_data[0], (int, float)) else len(grade_data) - 1
|
|
244
|
+
for c_idx in range(num_classes):
|
|
245
|
+
key = f"{g_idx + 1}학년 {c_idx + 1}반"
|
|
246
|
+
try:
|
|
247
|
+
result[key] = _parse_single_class(data, g_idx, c_idx)
|
|
248
|
+
except (IndexError, KeyError, TypeError):
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
return result
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: comci
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Comci.net 시간표 API 비공식 클라이언트
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: requests>=2.28.0
|
|
8
|
+
|
|
9
|
+
# Comci.net 시간표 API 클라이언트
|
|
10
|
+
|
|
11
|
+
컴시간(comci.net) 비공식 API를 사용하여 학교 검색 및 시간표 조회를 할 수 있는 Python 모듈입니다.
|
|
12
|
+
|
|
13
|
+
## 설치
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -r requirements.txt
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 사용법
|
|
20
|
+
|
|
21
|
+
### 1. 학교 검색 (지역, 학교명, 학교코드)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from comci import search_schools
|
|
25
|
+
|
|
26
|
+
# "신송" 검색
|
|
27
|
+
schools = search_schools("신송")
|
|
28
|
+
for s in schools:
|
|
29
|
+
print(s["region"], s["school_name"], s["school_code"])
|
|
30
|
+
# 인천 신송중학교 49654
|
|
31
|
+
# 인천 신송고등학교 51825
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. 특정 학년/반 시간표 조회
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from comci import get_timetable
|
|
38
|
+
|
|
39
|
+
# 1학년 1반 시간표 (정리.md 반환 구조)
|
|
40
|
+
timetable = get_timetable(49654, grade=1, class_num=1)
|
|
41
|
+
# {"월": [...], "화": [...], "수": [...], "목": [...], "금": [...]}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. 전체 학년/반 시간표 조회
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# 학년, 반 미지정 시 전체
|
|
48
|
+
timetable = get_timetable(49654)
|
|
49
|
+
# {"1학년 1반": {...}, "1학년 2반": {...}, ...}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 4. 검색 후 바로 시간표 조회
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from comci import search_and_get_timetable
|
|
56
|
+
|
|
57
|
+
timetable = search_and_get_timetable("신송", school_index=0, grade=1, class_num=1)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 프로젝트 구조
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
comci/
|
|
64
|
+
├── __init__.py # 패키지 진입점
|
|
65
|
+
├── config.py # API URL, 상수
|
|
66
|
+
├── school_search.py # 학교 검색
|
|
67
|
+
├── timetable.py # 시간표 API 및 파싱
|
|
68
|
+
└── client.py # 통합 클라이언트
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 예시 실행
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python example_usage.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 시간표 데이터 구조
|
|
78
|
+
|
|
79
|
+
각 요일별 리스트는 교시(1~8) 순서이며, 각 항목은:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
{
|
|
83
|
+
"period": 1, # 교시
|
|
84
|
+
"time": "08:40", # 시간
|
|
85
|
+
"subject": "국어", # 과목
|
|
86
|
+
"teacher": "김선생", # 교사
|
|
87
|
+
"room": "101", # 강의실
|
|
88
|
+
"changed": False # 변경 수업 여부
|
|
89
|
+
}
|
|
90
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
comci/__init__.py
|
|
4
|
+
comci/client.py
|
|
5
|
+
comci/config.py
|
|
6
|
+
comci/school_search.py
|
|
7
|
+
comci/timetable.py
|
|
8
|
+
comci.egg-info/PKG-INFO
|
|
9
|
+
comci.egg-info/SOURCES.txt
|
|
10
|
+
comci.egg-info/dependency_links.txt
|
|
11
|
+
comci.egg-info/requires.txt
|
|
12
|
+
comci.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
comci
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "comci"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Comci.net 시간표 API 비공식 클라이언트"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = ["requests>=2.28.0"]
|
|
12
|
+
|
|
13
|
+
[tool.setuptools]
|
|
14
|
+
packages = ["comci"]
|
|
15
|
+
|
comci-0.1.0/setup.cfg
ADDED