ucsd-study-room 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Theo Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: ucsd-study-room
3
+ Version: 0.1.0
4
+ Summary: CLI & MCP tool to automatically search and book UCSD study rooms via EMS
5
+ Author: Theo Lee
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/TheoLee021/ucsd-study-room
8
+ Project-URL: Issues, https://github.com/TheoLee021/ucsd-study-room/issues
9
+ Keywords: ucsd,study-room,ems,booking,playwright,mcp
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: playwright>=1.40
22
+ Requires-Dist: typer>=0.9
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: mcp>=1.0
25
+ Requires-Dist: rich>=13.0
26
+ Requires-Dist: keyring>=25.0
27
+ Dynamic: license-file
28
+
29
+ # ucsd-study-room
30
+
31
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
33
+
34
+ A CLI tool and MCP server that automatically searches and books UCSD Price Center study rooms (Rooms 1--8) through the EMS Cloud booking system.
35
+
36
+ ## Features
37
+
38
+ - **Headless browser automation** -- Searches and books rooms using Playwright with real Chrome, no browser window required
39
+ - **UCSD SSO + Duo Push authentication** -- Handles SAML-based single sign-on and Duo two-factor authentication
40
+ - **Session persistence** -- Saves browser sessions (cookies + localStorage) for reuse; credentials stored securely in macOS Keychain
41
+ - **Automatic re-authentication** -- When SSO expires, opens a headed browser for Duo Push re-verification without requiring you to re-enter credentials
42
+ - **CLI interface** -- Simple `study-room` command for searching and booking from the terminal
43
+ - **MCP server** -- Integrates with Claude Code so you can book rooms using natural language
44
+
45
+ ## Requirements
46
+
47
+ - Python 3.11 or later
48
+ - Google Chrome installed
49
+ - UCSD account with Duo Push enabled
50
+ - macOS (uses Keychain for credential storage)
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install ucsd-study-room
56
+ playwright install chromium
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ **1. Log in with your UCSD credentials (first time only):**
62
+
63
+ ```bash
64
+ study-room login
65
+ ```
66
+
67
+ A Chrome window will open. Enter your UCSD SSO credentials when prompted, then approve the Duo Push notification on your phone. Your session and credentials are saved for future use.
68
+
69
+ **2. Search for available rooms:**
70
+
71
+ ```bash
72
+ study-room search --date 2026-03-11 --start 15:00 --end 17:00
73
+ ```
74
+
75
+ **3. Search and book interactively:**
76
+
77
+ ```bash
78
+ study-room search --date 2026-03-11 --start 15:00 --end 17:00 --book
79
+ ```
80
+
81
+ ## CLI Commands
82
+
83
+ | Command | Description |
84
+ |---------|-------------|
85
+ | `study-room login` | SSO login with Duo Push (opens browser for first-time auth) |
86
+ | `study-room search` | Search available rooms with `--date`, `--start`, `--end` options |
87
+ | `study-room search --book` | Search and book a room interactively |
88
+ | `study-room config` | View or set user info (`--name`, `--email`, `--attendees`) |
89
+ | `study-room status` | Check whether the current session is valid |
90
+
91
+ ### Examples
92
+
93
+ ```bash
94
+ # Set your contact info (required before booking)
95
+ study-room config --name "Your Name" --email "you@ucsd.edu"
96
+
97
+ # Search for rooms on a specific date and time
98
+ study-room search --date 2026-03-11 --start 14:00 --end 16:00
99
+
100
+ # Search and book in one step
101
+ study-room search --date 2026-03-11 --start 14:00 --end 16:00 --book
102
+
103
+ # Check if your session is still active
104
+ study-room status
105
+ ```
106
+
107
+ ## MCP Server (Claude Code Integration)
108
+
109
+ The MCP server lets you search and book study rooms using natural language through Claude Code.
110
+
111
+ ### Setup
112
+
113
+ Add the following to your `.claude/settings.json`:
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "study-room": {
119
+ "command": "python",
120
+ "args": ["-m", "study_room.mcp_server"]
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ### Usage
127
+
128
+ Once configured, you can use natural language in Claude Code:
129
+
130
+ - "Search for available study rooms tomorrow from 2pm to 4pm"
131
+ - "Book Price Center Study Room 3 on March 11 from 3pm to 5pm"
132
+ - "Are there any rooms open this Friday afternoon?"
133
+
134
+ ### Available MCP Tools
135
+
136
+ | Tool | Description |
137
+ |------|-------------|
138
+ | `search_rooms` | Search for available rooms by date and time range |
139
+ | `book_room` | Book a specific room (use after `search_rooms`) |
140
+ | `login` | Authenticate via UCSD SSO + Duo Push |
141
+
142
+ ## How It Works
143
+
144
+ 1. **Browser automation** -- Uses Playwright with real Chrome (`channel="chrome"`) in headless mode to interact with the EMS Cloud booking system.
145
+ 2. **Authentication** -- Navigates to the UCSD SAML SSO page, submits credentials, and waits for Duo Push approval. On first login, a headed browser window opens for the Duo flow.
146
+ 3. **Session management** -- After authentication, cookies and browser storage state are saved to `~/.study-room/`. Credentials are stored in macOS Keychain via the `keyring` library. Sessions are valid for 7 days.
147
+ 4. **Auto re-login** -- When a session expires during a search or booking operation, the tool automatically opens a headed browser, loads credentials from Keychain, and re-authenticates with Duo Push.
148
+ 5. **Room search** -- Navigates to the EMS booking page, fills in date and time fields, and parses available rooms by inspecting the DOM for booking buttons.
149
+ 6. **Booking** -- Clicks the add-to-cart button for the selected room, fills in the reservation form (name, email, terms), and submits the reservation.
150
+
151
+ ## Configuration
152
+
153
+ Configuration is stored in `~/.study-room/config.yaml`. Default target rooms are Price Center Study Room 1 through 8.
154
+
155
+ ```yaml
156
+ name: "Your Name"
157
+ email: "you@ucsd.edu"
158
+ default_attendees: 1
159
+ rooms:
160
+ - Price Center Study Room 1
161
+ - Price Center Study Room 2
162
+ - Price Center Study Room 3
163
+ - Price Center Study Room 4
164
+ - Price Center Study Room 5
165
+ - Price Center Study Room 6
166
+ - Price Center Study Room 7
167
+ - Price Center Study Room 8
168
+ ```
169
+
170
+ ## Contributing
171
+
172
+ Contributions are welcome. To contribute:
173
+
174
+ 1. Fork the repository
175
+ 2. Create a feature branch (`git checkout -b feature/your-feature`)
176
+ 3. Commit your changes
177
+ 4. Push to the branch and open a pull request
178
+
179
+ Please make sure existing tests pass before submitting.
180
+
181
+ ## License
182
+
183
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,155 @@
1
+ # ucsd-study-room
2
+
3
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A CLI tool and MCP server that automatically searches and books UCSD Price Center study rooms (Rooms 1--8) through the EMS Cloud booking system.
7
+
8
+ ## Features
9
+
10
+ - **Headless browser automation** -- Searches and books rooms using Playwright with real Chrome, no browser window required
11
+ - **UCSD SSO + Duo Push authentication** -- Handles SAML-based single sign-on and Duo two-factor authentication
12
+ - **Session persistence** -- Saves browser sessions (cookies + localStorage) for reuse; credentials stored securely in macOS Keychain
13
+ - **Automatic re-authentication** -- When SSO expires, opens a headed browser for Duo Push re-verification without requiring you to re-enter credentials
14
+ - **CLI interface** -- Simple `study-room` command for searching and booking from the terminal
15
+ - **MCP server** -- Integrates with Claude Code so you can book rooms using natural language
16
+
17
+ ## Requirements
18
+
19
+ - Python 3.11 or later
20
+ - Google Chrome installed
21
+ - UCSD account with Duo Push enabled
22
+ - macOS (uses Keychain for credential storage)
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install ucsd-study-room
28
+ playwright install chromium
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ **1. Log in with your UCSD credentials (first time only):**
34
+
35
+ ```bash
36
+ study-room login
37
+ ```
38
+
39
+ A Chrome window will open. Enter your UCSD SSO credentials when prompted, then approve the Duo Push notification on your phone. Your session and credentials are saved for future use.
40
+
41
+ **2. Search for available rooms:**
42
+
43
+ ```bash
44
+ study-room search --date 2026-03-11 --start 15:00 --end 17:00
45
+ ```
46
+
47
+ **3. Search and book interactively:**
48
+
49
+ ```bash
50
+ study-room search --date 2026-03-11 --start 15:00 --end 17:00 --book
51
+ ```
52
+
53
+ ## CLI Commands
54
+
55
+ | Command | Description |
56
+ |---------|-------------|
57
+ | `study-room login` | SSO login with Duo Push (opens browser for first-time auth) |
58
+ | `study-room search` | Search available rooms with `--date`, `--start`, `--end` options |
59
+ | `study-room search --book` | Search and book a room interactively |
60
+ | `study-room config` | View or set user info (`--name`, `--email`, `--attendees`) |
61
+ | `study-room status` | Check whether the current session is valid |
62
+
63
+ ### Examples
64
+
65
+ ```bash
66
+ # Set your contact info (required before booking)
67
+ study-room config --name "Your Name" --email "you@ucsd.edu"
68
+
69
+ # Search for rooms on a specific date and time
70
+ study-room search --date 2026-03-11 --start 14:00 --end 16:00
71
+
72
+ # Search and book in one step
73
+ study-room search --date 2026-03-11 --start 14:00 --end 16:00 --book
74
+
75
+ # Check if your session is still active
76
+ study-room status
77
+ ```
78
+
79
+ ## MCP Server (Claude Code Integration)
80
+
81
+ The MCP server lets you search and book study rooms using natural language through Claude Code.
82
+
83
+ ### Setup
84
+
85
+ Add the following to your `.claude/settings.json`:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "study-room": {
91
+ "command": "python",
92
+ "args": ["-m", "study_room.mcp_server"]
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Usage
99
+
100
+ Once configured, you can use natural language in Claude Code:
101
+
102
+ - "Search for available study rooms tomorrow from 2pm to 4pm"
103
+ - "Book Price Center Study Room 3 on March 11 from 3pm to 5pm"
104
+ - "Are there any rooms open this Friday afternoon?"
105
+
106
+ ### Available MCP Tools
107
+
108
+ | Tool | Description |
109
+ |------|-------------|
110
+ | `search_rooms` | Search for available rooms by date and time range |
111
+ | `book_room` | Book a specific room (use after `search_rooms`) |
112
+ | `login` | Authenticate via UCSD SSO + Duo Push |
113
+
114
+ ## How It Works
115
+
116
+ 1. **Browser automation** -- Uses Playwright with real Chrome (`channel="chrome"`) in headless mode to interact with the EMS Cloud booking system.
117
+ 2. **Authentication** -- Navigates to the UCSD SAML SSO page, submits credentials, and waits for Duo Push approval. On first login, a headed browser window opens for the Duo flow.
118
+ 3. **Session management** -- After authentication, cookies and browser storage state are saved to `~/.study-room/`. Credentials are stored in macOS Keychain via the `keyring` library. Sessions are valid for 7 days.
119
+ 4. **Auto re-login** -- When a session expires during a search or booking operation, the tool automatically opens a headed browser, loads credentials from Keychain, and re-authenticates with Duo Push.
120
+ 5. **Room search** -- Navigates to the EMS booking page, fills in date and time fields, and parses available rooms by inspecting the DOM for booking buttons.
121
+ 6. **Booking** -- Clicks the add-to-cart button for the selected room, fills in the reservation form (name, email, terms), and submits the reservation.
122
+
123
+ ## Configuration
124
+
125
+ Configuration is stored in `~/.study-room/config.yaml`. Default target rooms are Price Center Study Room 1 through 8.
126
+
127
+ ```yaml
128
+ name: "Your Name"
129
+ email: "you@ucsd.edu"
130
+ default_attendees: 1
131
+ rooms:
132
+ - Price Center Study Room 1
133
+ - Price Center Study Room 2
134
+ - Price Center Study Room 3
135
+ - Price Center Study Room 4
136
+ - Price Center Study Room 5
137
+ - Price Center Study Room 6
138
+ - Price Center Study Room 7
139
+ - Price Center Study Room 8
140
+ ```
141
+
142
+ ## Contributing
143
+
144
+ Contributions are welcome. To contribute:
145
+
146
+ 1. Fork the repository
147
+ 2. Create a feature branch (`git checkout -b feature/your-feature`)
148
+ 3. Commit your changes
149
+ 4. Push to the branch and open a pull request
150
+
151
+ Please make sure existing tests pass before submitting.
152
+
153
+ ## License
154
+
155
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "ucsd-study-room"
3
+ version = "0.1.0"
4
+ description = "CLI & MCP tool to automatically search and book UCSD study rooms via EMS"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.11"
8
+ authors = [
9
+ {name = "Theo Lee"}
10
+ ]
11
+ keywords = ["ucsd", "study-room", "ems", "booking", "playwright", "mcp"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Education",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Utilities",
21
+ ]
22
+ dependencies = [
23
+ "playwright>=1.40",
24
+ "typer>=0.9",
25
+ "pyyaml>=6.0",
26
+ "mcp>=1.0",
27
+ "rich>=13.0",
28
+ "keyring>=25.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/TheoLee021/ucsd-study-room"
33
+ Issues = "https://github.com/TheoLee021/ucsd-study-room/issues"
34
+
35
+ [project.scripts]
36
+ study-room = "study_room.cli:app"
37
+
38
+ [build-system]
39
+ requires = ["setuptools>=68.0"]
40
+ build-backend = "setuptools.build_meta"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["study_room*"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,184 @@
1
+ import json
2
+ from datetime import datetime, timedelta
3
+ from pathlib import Path
4
+
5
+ import keyring
6
+ from playwright.async_api import async_playwright
7
+
8
+ SESSION_DIR = Path.home() / ".study-room"
9
+ SESSION_PATH = SESSION_DIR / "session.json"
10
+ STORAGE_STATE_PATH = SESSION_DIR / "storage_state.json"
11
+ SESSION_MAX_AGE_DAYS = 7
12
+ EMS_URL = "https://ucsdevents.emscloudservice.com/web/"
13
+ SAML_URL = "https://ucsdevents.emscloudservice.com/web/samlauth.aspx"
14
+ DUO_TIMEOUT_MS = 60_000
15
+ KEYRING_SERVICE = "study-room-booking"
16
+ KEYRING_USERNAME_KEY = "ucsd-sso-username"
17
+
18
+
19
+ def save_credentials(username: str, password: str) -> None:
20
+ keyring.set_password(KEYRING_SERVICE, KEYRING_USERNAME_KEY, username)
21
+ keyring.set_password(KEYRING_SERVICE, username, password)
22
+
23
+
24
+ def load_credentials() -> tuple[str, str] | None:
25
+ username = keyring.get_password(KEYRING_SERVICE, KEYRING_USERNAME_KEY)
26
+ if username is None:
27
+ return None
28
+ password = keyring.get_password(KEYRING_SERVICE, username)
29
+ if password is None:
30
+ return None
31
+ return username, password
32
+
33
+
34
+ def save_session(cookies: list, path: Path = SESSION_PATH) -> None:
35
+ path.parent.mkdir(parents=True, exist_ok=True)
36
+ data = {
37
+ "cookies": cookies,
38
+ "created_at": datetime.now().isoformat(),
39
+ }
40
+ path.write_text(json.dumps(data, indent=2))
41
+
42
+
43
+ def load_session(path: Path = SESSION_PATH) -> dict | None:
44
+ if not path.exists():
45
+ return None
46
+ return json.loads(path.read_text())
47
+
48
+
49
+ def is_session_valid(path: Path = SESSION_PATH) -> bool:
50
+ session = load_session(path)
51
+ if session is None:
52
+ return False
53
+ created = datetime.fromisoformat(session["created_at"])
54
+ return datetime.now() - created < timedelta(days=SESSION_MAX_AGE_DAYS)
55
+
56
+
57
+ async def login(username: str, password: str, session_path: Path = SESSION_PATH) -> list:
58
+ """SSO 로그인 + Duo Push 인증 후 쿠키를 저장한다."""
59
+ async with async_playwright() as p:
60
+ browser = await p.chromium.launch(headless=False, channel="chrome")
61
+ context = await browser.new_context()
62
+ page = await context.new_page()
63
+
64
+ # 1. SAML 인증 페이지로 직접 이동 → UCSD SSO 리다이렉트
65
+ await page.goto(SAML_URL)
66
+ await page.wait_for_load_state("networkidle")
67
+
68
+ # 2. UCSD SSO 로그인 — username + password
69
+ await page.wait_for_selector("#ssousername", timeout=15000)
70
+ await page.locator("#ssousername").fill(username)
71
+ await page.locator("#ssopassword").fill(password)
72
+ await page.locator("button[type='submit']").click()
73
+
74
+ # 3. Duo Push 대기 — EMS 페이지로 돌아오면 인증 완료
75
+ print("Duo Push가 전송되었습니다. 폰에서 승인해주세요...")
76
+ await page.wait_for_url("**/web/**", timeout=DUO_TIMEOUT_MS)
77
+
78
+ # 4. credentials를 keychain에 저장
79
+ save_credentials(username, password)
80
+
81
+ # 5. storage state 전체 저장 (쿠키 + localStorage + sessionStorage)
82
+ cookies = await context.cookies()
83
+ save_session(cookies, session_path)
84
+
85
+ storage_state_path = session_path.parent / "storage_state.json"
86
+ await context.storage_state(path=str(storage_state_path))
87
+ print(f"로그인 성공! {len(cookies)}개 쿠키 + storage state 저장됨.")
88
+
89
+ await browser.close()
90
+ return cookies
91
+
92
+
93
+ async def get_authenticated_context(playwright, session_path: Path = SESSION_PATH, headless: bool = True, channel: str | None = "chrome"):
94
+ """저장된 storage state로 인증된 브라우저 컨텍스트를 반환한다."""
95
+ if not is_session_valid(session_path):
96
+ return None
97
+
98
+ storage_state_path = session_path.parent / "storage_state.json"
99
+ launch_args = {"headless": headless}
100
+ if channel:
101
+ launch_args["channel"] = channel
102
+ browser = await playwright.chromium.launch(**launch_args)
103
+
104
+ if storage_state_path.exists():
105
+ context = await browser.new_context(storage_state=str(storage_state_path))
106
+ else:
107
+ session = load_session(session_path)
108
+ context = await browser.new_context()
109
+ await context.add_cookies(session["cookies"])
110
+
111
+ return context
112
+
113
+
114
+ async def _headed_login_and_save(session_path: Path = SESSION_PATH):
115
+ """SSO 만료 시 headed 브라우저로 재로그인. Keychain → 자동입력, 실패 → 수동입력."""
116
+ import asyncio
117
+
118
+ creds = load_credentials()
119
+
120
+ async with async_playwright() as p:
121
+ browser = await p.chromium.launch(headless=False, channel="chrome")
122
+ context = await browser.new_context()
123
+ page = await context.new_page()
124
+
125
+ await page.goto(SAML_URL)
126
+ await page.wait_for_load_state("networkidle")
127
+
128
+ await page.wait_for_selector("#ssousername", timeout=15000)
129
+
130
+ if creds:
131
+ username, password = creds
132
+ print("Keychain에서 credentials 로드 → 자동 입력 중...")
133
+ await page.locator("#ssousername").fill(username)
134
+ await page.locator("#ssopassword").fill(password)
135
+ await page.locator("button[type='submit']").click()
136
+ else:
137
+ print("Keychain에 credentials 없음 → 브라우저에서 직접 로그인해주세요.")
138
+
139
+ print("Duo Push가 전송되었습니다. 폰에서 승인해주세요...")
140
+ await page.wait_for_url("**/web/**", timeout=DUO_TIMEOUT_MS)
141
+
142
+ # 세션 저장
143
+ cookies = await context.cookies()
144
+ save_session(cookies, session_path)
145
+ storage_state_path = session_path.parent / "storage_state.json"
146
+ await context.storage_state(path=str(storage_state_path))
147
+ print("재로그인 성공! 세션 갱신됨.")
148
+
149
+ await browser.close()
150
+
151
+
152
+ async def authenticate(page, session_path: Path = SESSION_PATH):
153
+ """SAML 인증. SSO 유효 시 자동 통과, 만료 시 headed 브라우저로 Duo 인증."""
154
+ import asyncio
155
+
156
+ print("SAML 인증 중...")
157
+
158
+ await page.goto(SAML_URL)
159
+ await page.wait_for_load_state("networkidle")
160
+ await asyncio.sleep(2)
161
+
162
+ # SSO 로그인 페이지가 나왔는지 확인
163
+ sso_form = page.locator("#ssousername")
164
+ if await sso_form.count() > 0:
165
+ # SSO 만료 — headed 브라우저로 Duo 인증
166
+ print("SSO 세션 만료. headed 브라우저로 로그인 진행...")
167
+ await page.context.browser.close()
168
+ await _headed_login_and_save(session_path)
169
+ return "relogin_needed"
170
+
171
+ # SSO 유효 → 자동으로 EMS 리다이렉트
172
+ await page.wait_for_url("**/web/**", timeout=15000)
173
+
174
+ # storage state 갱신
175
+ context = page.context
176
+ cookies = await context.cookies()
177
+ save_session(cookies, session_path)
178
+ storage_state_path = session_path.parent / "storage_state.json"
179
+ await context.storage_state(path=str(storage_state_path))
180
+ print("인증 완료.")
181
+
182
+
183
+ class SessionExpiredError(Exception):
184
+ pass