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.
- ucsd_study_room-0.1.0/LICENSE +21 -0
- ucsd_study_room-0.1.0/PKG-INFO +183 -0
- ucsd_study_room-0.1.0/README.md +155 -0
- ucsd_study_room-0.1.0/pyproject.toml +46 -0
- ucsd_study_room-0.1.0/setup.cfg +4 -0
- ucsd_study_room-0.1.0/study_room/__init__.py +0 -0
- ucsd_study_room-0.1.0/study_room/auth.py +184 -0
- ucsd_study_room-0.1.0/study_room/booking.py +260 -0
- ucsd_study_room-0.1.0/study_room/cli.py +120 -0
- ucsd_study_room-0.1.0/study_room/config.py +30 -0
- ucsd_study_room-0.1.0/study_room/mcp_server.py +111 -0
- ucsd_study_room-0.1.0/tests/test_auth.py +37 -0
- ucsd_study_room-0.1.0/tests/test_config.py +42 -0
- ucsd_study_room-0.1.0/tests/test_headless_search.py +94 -0
- ucsd_study_room-0.1.0/ucsd_study_room.egg-info/PKG-INFO +183 -0
- ucsd_study_room-0.1.0/ucsd_study_room.egg-info/SOURCES.txt +18 -0
- ucsd_study_room-0.1.0/ucsd_study_room.egg-info/dependency_links.txt +1 -0
- ucsd_study_room-0.1.0/ucsd_study_room.egg-info/entry_points.txt +2 -0
- ucsd_study_room-0.1.0/ucsd_study_room.egg-info/requires.txt +6 -0
- ucsd_study_room-0.1.0/ucsd_study_room.egg-info/top_level.txt +1 -0
|
@@ -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
|
+
[](https://www.python.org/downloads/)
|
|
32
|
+
[](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
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](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"]
|
|
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
|