chuvsu-js 4.2.0 → 5.0.1
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.
- package/README.md +165 -193
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/common/cache.js +30 -9
- package/dist/common/http.d.ts +6 -0
- package/dist/common/http.js +8 -1
- package/dist/common/parse.d.ts +1 -1
- package/dist/common/parse.js +2 -2
- package/dist/common/types.d.ts +8 -2
- package/dist/common/types.js +12 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -4
- package/dist/lk/client.d.ts +8 -6
- package/dist/lk/client.js +51 -23
- package/dist/lk/types.d.ts +7 -7
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/parsers.d.ts +3 -0
- package/dist/parsers.js +2 -0
- package/dist/tt/client.d.ts +55 -63
- package/dist/tt/client.js +477 -226
- package/dist/tt/domain/directory.d.ts +14 -0
- package/dist/tt/domain/directory.js +116 -0
- package/dist/tt/domain/ids.d.ts +7 -0
- package/dist/tt/domain/ids.js +20 -0
- package/dist/tt/domain/index.d.ts +7 -0
- package/dist/tt/domain/index.js +5 -0
- package/dist/tt/domain/normalize.d.ts +7 -0
- package/dist/tt/domain/normalize.js +65 -0
- package/dist/tt/domain/repository.d.ts +51 -0
- package/dist/tt/domain/repository.js +805 -0
- package/dist/tt/domain/schedule.d.ts +37 -0
- package/dist/tt/domain/schedule.js +292 -0
- package/dist/tt/domain/types.d.ts +175 -0
- package/dist/tt/domain/types.js +1 -0
- package/dist/tt/observations.d.ts +11 -0
- package/dist/tt/observations.js +131 -0
- package/dist/tt/parse/audience.d.ts +3 -3
- package/dist/tt/parse/audience.js +47 -15
- package/dist/tt/parse/entry-parts.d.ts +1 -0
- package/dist/tt/parse/entry-parts.js +5 -2
- package/dist/tt/parse/full-schedule.d.ts +9 -4
- package/dist/tt/parse/full-schedule.js +32 -41
- package/dist/tt/parse/groups.d.ts +1 -1
- package/dist/tt/parse/groups.js +1 -1
- package/dist/tt/parse/index.d.ts +4 -4
- package/dist/tt/parse/index.js +4 -4
- package/dist/tt/parse/lists.d.ts +6 -5
- package/dist/tt/parse/lists.js +7 -2
- package/dist/tt/parse/overlays.d.ts +5 -4
- package/dist/tt/parse/overlays.js +25 -18
- package/dist/tt/parse/teacher.d.ts +2 -3
- package/dist/tt/parse/teacher.js +14 -56
- package/dist/tt/parse/webinars.js +3 -4
- package/dist/tt/types.d.ts +54 -69
- package/dist/tt/utils/date.d.ts +4 -0
- package/dist/tt/utils/date.js +30 -1
- package/dist/tt/utils/index.d.ts +2 -3
- package/dist/tt/utils/index.js +2 -3
- package/dist/tt/utils/period.d.ts +4 -4
- package/dist/tt/utils/period.js +9 -8
- package/dist/tt/utils/semester.d.ts +4 -4
- package/dist/tt/utils/semester.js +25 -5
- package/dist/tt/utils/time-slots.d.ts +3 -5
- package/dist/tt/utils/time-slots.js +19 -34
- package/dist/tt/webinars.d.ts +7 -0
- package/dist/tt/webinars.js +38 -0
- package/docs/fixture-review.md +56 -0
- package/docs/testing.md +23 -0
- package/docs/v5-architecture.md +144 -0
- package/docs/v5-migration.md +451 -0
- package/package.json +17 -7
- package/dist/shared.d.ts +0 -9
- package/dist/shared.js +0 -7
- package/dist/tt/schedule.d.ts +0 -55
- package/dist/tt/schedule.js +0 -233
- package/dist/tt/utils/lessons.d.ts +0 -16
- package/dist/tt/utils/lessons.js +0 -173
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# chuvsu-js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Типизированная библиотека для сервисов ЧГУ им. И. Н. Ульянова:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
5
|
+
- `tt.chuvsu.ru` — расписания групп, преподавателей, аудиторий и вебинары;
|
|
6
|
+
- `lk.chuvsu.ru` — личный кабинет студента.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
В версии 5 расписание хранится в едином каноническом репозитории. Если одна
|
|
9
|
+
пара встречается в расписаниях группы, преподавателя и аудитории, она сохраняет
|
|
10
|
+
один ID, а сведения из разных страниц дополняют друг друга.
|
|
11
|
+
|
|
12
|
+
Переход с версии 4: [`docs/v5-migration.md`](docs/v5-migration.md).
|
|
12
13
|
|
|
13
14
|
## Установка
|
|
14
15
|
|
|
@@ -16,286 +17,257 @@ Node.js библиотека для работы с порталами ЧувГ
|
|
|
16
17
|
npm install chuvsu-js
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
Рекомендуются Node.js 20+ и ESM.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
## Быстрый старт
|
|
22
23
|
|
|
23
24
|
```ts
|
|
24
|
-
import {
|
|
25
|
+
import { TimetableClient } from "chuvsu-js";
|
|
25
26
|
|
|
26
|
-
const
|
|
27
|
+
const client = new TimetableClient({ cache: 15 * 60_000 });
|
|
28
|
+
await client.loginAsGuest();
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
await
|
|
30
|
+
const [group] = await client.searchGroups("КТ-41-24");
|
|
31
|
+
const schedule = await client.getGroupSchedule(group.id);
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
const groups = await tt.searchGroup({ name: "КТ-41-24" });
|
|
33
|
-
console.log(groups); // [{ id: 8919, name: "КТ-41-24", specialty: "...", profile: "..." }]
|
|
34
|
-
|
|
35
|
-
// Получить расписание группы
|
|
36
|
-
const schedule = await tt.getSchedule({ groupId: groups[0].id });
|
|
37
|
-
|
|
38
|
-
// Расписание на сегодня
|
|
39
|
-
const today = schedule.today();
|
|
40
|
-
for (const lesson of today) {
|
|
33
|
+
for (const lesson of schedule.today({ subgroup: 1 })) {
|
|
41
34
|
console.log(
|
|
42
|
-
|
|
35
|
+
lesson.id,
|
|
36
|
+
lesson.subject,
|
|
37
|
+
lesson.teachers.values,
|
|
38
|
+
lesson.rooms.values,
|
|
43
39
|
);
|
|
44
40
|
}
|
|
45
|
-
|
|
46
|
-
// С фильтром по подгруппе
|
|
47
|
-
schedule.today({ subgroup: 1 });
|
|
48
|
-
|
|
49
|
-
// На завтра
|
|
50
|
-
schedule.tomorrow();
|
|
51
|
-
|
|
52
|
-
// На текущую неделю
|
|
53
|
-
schedule.thisWeek();
|
|
54
|
-
|
|
55
|
-
// Текущая пара
|
|
56
|
-
schedule.currentLesson();
|
|
57
41
|
```
|
|
58
42
|
|
|
59
|
-
|
|
43
|
+
У каждой конкретной пары есть `id`. У повторяющейся пары также есть
|
|
44
|
+
`seriesId`:
|
|
60
45
|
|
|
61
46
|
```ts
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const data = await lk.getPersonalData();
|
|
68
|
-
console.log(`${data.lastName} ${data.firstName}, группа ${data.group}`);
|
|
69
|
-
|
|
70
|
-
// Получить ID группы для использования с TtClient
|
|
71
|
-
const groupId = await lk.getGroupId();
|
|
47
|
+
const series = schedule.series();
|
|
48
|
+
const monday = schedule.weekday(1, { week: 4 });
|
|
49
|
+
const day = schedule.on(new Date(2026, 8, 7));
|
|
50
|
+
const week = schedule.week(4);
|
|
51
|
+
const current = schedule.current();
|
|
72
52
|
```
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Клиент для работы с расписанием (`tt.chuvsu.ru`).
|
|
54
|
+
ID не строится из даты, аудитории или времени. Перенос и смена аудитории сами
|
|
55
|
+
по себе не создают новую сущность. Чтобы ID сохранялись между запусками,
|
|
56
|
+
подключите постоянное хранилище репозитория.
|
|
79
57
|
|
|
80
|
-
|
|
58
|
+
## Расписания разных владельцев
|
|
81
59
|
|
|
82
60
|
```ts
|
|
83
|
-
|
|
61
|
+
await client.getGroupSchedule(groupId);
|
|
62
|
+
await client.getTeacherSchedule(teacherId);
|
|
63
|
+
await client.getRoomSchedule(roomId);
|
|
84
64
|
```
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
| --------------- | ----------------------- | ----------------- | -------------------------------------------------------------- |
|
|
88
|
-
| `educationType` | `EducationType` | `HigherEducation` | Тип образования: высшее (1) или СПО (2) |
|
|
89
|
-
| `cache` | `number \| CacheConfig` | — | TTL кеша в мс. Число задаёт единый TTL, объект — по категориям |
|
|
90
|
-
| `cacheAdapter` | `CacheAdapter` | — | Внешний L2-кеш для JSON-данных (например Redis/БД) |
|
|
91
|
-
| `blobAdapter` | `BlobAdapter` | — | Хранилище бинарных данных, например S3/R2/MinIO |
|
|
92
|
-
|
|
93
|
-
#### Авторизация
|
|
66
|
+
Общий метод:
|
|
94
67
|
|
|
95
68
|
```ts
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await tt.loginAsGuest();
|
|
69
|
+
await client.getSchedule({
|
|
70
|
+
type: "teacher",
|
|
71
|
+
teacher: { id: teacherId, name: "Иванов Иван Иванович" },
|
|
72
|
+
});
|
|
101
73
|
```
|
|
102
74
|
|
|
103
|
-
|
|
75
|
+
Можно загрузить только нужные периоды:
|
|
104
76
|
|
|
105
77
|
```ts
|
|
106
|
-
|
|
78
|
+
import { AcademicPeriod } from "chuvsu-js";
|
|
79
|
+
|
|
80
|
+
await client.getGroupSchedule(groupId, {
|
|
81
|
+
periods: [AcademicPeriod.FallSemester],
|
|
82
|
+
});
|
|
107
83
|
```
|
|
108
84
|
|
|
109
|
-
|
|
85
|
+
## Дополнение данных из разных страниц
|
|
110
86
|
|
|
111
|
-
|
|
87
|
+
Страница группы часто содержит только фамилию и инициалы преподавателя.
|
|
88
|
+
Страница преподавателя сообщает список групп, а страница аудитории — другие
|
|
89
|
+
связи той же пары. Все это считается неполными наблюдениями:
|
|
112
90
|
|
|
113
91
|
```ts
|
|
114
|
-
|
|
115
|
-
const
|
|
92
|
+
const groupSchedule = await client.getGroupSchedule(groupId);
|
|
93
|
+
const before = groupSchedule.on(date);
|
|
116
94
|
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
await client.getTeacherSchedule(teacherId);
|
|
96
|
+
await client.getRoomSchedule(roomId);
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Поиск преподавателя
|
|
124
|
-
const teachers = await tt.searchTeacher({ name: "Иванов" });
|
|
98
|
+
const after = groupSchedule.on(date);
|
|
99
|
+
// ID прежний, но в связях могли появиться дополнительные группы и ID сущностей.
|
|
125
100
|
```
|
|
126
101
|
|
|
127
|
-
|
|
102
|
+
`Schedule` — живое представление репозитория: следующий запрос через уже
|
|
103
|
+
созданный объект видит новые данные.
|
|
128
104
|
|
|
129
|
-
|
|
130
|
-
// Текущий учебный период
|
|
131
|
-
const period = tt.getCurrentPeriod();
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
#### Кеш
|
|
105
|
+
Связи имеют вид `RelationSet<T>`:
|
|
135
106
|
|
|
136
107
|
```ts
|
|
137
|
-
//
|
|
138
|
-
tt.clearCache();
|
|
139
|
-
tt.clearCache("schedule");
|
|
140
|
-
|
|
141
|
-
// Экспорт/импорт (для сохранения между запусками)
|
|
142
|
-
const data = tt.exportCache();
|
|
143
|
-
tt.importCache(data);
|
|
108
|
+
lesson.rooms // { values: RoomRef[], completeness: "unknown" | "partial" | "complete" }
|
|
144
109
|
```
|
|
145
110
|
|
|
146
|
-
|
|
111
|
+
`unknown` означает, что страница ничего не сообщила. `complete` с пустым
|
|
112
|
+
`values` означает, что отсутствие известно явно. Поэтому пустая строка
|
|
113
|
+
аудитории в канонической модели не используется.
|
|
147
114
|
|
|
148
|
-
|
|
149
|
-
- L1: in-memory кеш внутри процесса
|
|
150
|
-
- L2: внешний адаптер (`cacheAdapter`)
|
|
115
|
+
## Номер пары и время
|
|
151
116
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
### Schedule
|
|
155
|
-
|
|
156
|
-
Объект расписания группы. Все методы синхронные — данные уже загружены.
|
|
157
|
-
|
|
158
|
-
#### Свойства
|
|
117
|
+
Портал иногда показывает противоречивые номер пары и время. В v5 это независимые
|
|
118
|
+
утверждения:
|
|
159
119
|
|
|
160
120
|
```ts
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
schedule.days; // Сырые данные (FullScheduleDay[])
|
|
121
|
+
lesson.slotNumber // 6
|
|
122
|
+
lesson.time // { start: { hours: 16, minutes: 40 }, end: ... }
|
|
164
123
|
```
|
|
165
124
|
|
|
166
|
-
|
|
125
|
+
Номер не вычисляется из времени, а время — из номера. При объединении одна
|
|
126
|
+
совпавшая величина может подтвердить пару, если остальные признаки надежны;
|
|
127
|
+
разные дни или несовместимое чередование объединяться не будут.
|
|
167
128
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
schedule.today({ subgroup?: number });
|
|
129
|
+
Даты без времени представлены строкой `LocalDate` формата `YYYY-MM-DD`, например
|
|
130
|
+
`lesson.scheduledDate === "2026-09-03"`. Это исключает сдвиги даты из-за UTC.
|
|
171
131
|
|
|
172
|
-
|
|
173
|
-
|
|
132
|
+
Экзамены, консультации и другие строки сессии являются сразу конкретными
|
|
133
|
+
`LessonOccurrence`: дата берется со страницы, `seriesId` и `recurrence` у них
|
|
134
|
+
нет. Значения-заглушки наподобие `weeks: { from: 0, to: 0 }` не используются.
|
|
174
135
|
|
|
175
|
-
|
|
176
|
-
schedule.forDate(date: Date, { subgroup?: number });
|
|
177
|
-
```
|
|
136
|
+
## Справочник сущностей без каскадных запросов
|
|
178
137
|
|
|
179
|
-
|
|
138
|
+
Загрузка расписания никогда автоматически не запускает поиск преподавателей,
|
|
139
|
+
групп или аудиторий. Обычные методы поиска наполняют общий справочник:
|
|
180
140
|
|
|
181
141
|
```ts
|
|
182
|
-
|
|
183
|
-
|
|
142
|
+
await client.getTeachers();
|
|
143
|
+
await client.getRooms();
|
|
144
|
+
await client.getFacultyGroups(facultyId);
|
|
184
145
|
|
|
185
|
-
|
|
186
|
-
schedule.forWeek(week?: number, { subgroup?: number });
|
|
146
|
+
const teacher = await client.resolveTeacher("Иванов И. И.");
|
|
187
147
|
```
|
|
188
148
|
|
|
189
|
-
|
|
149
|
+
По умолчанию разрешение использует только уже известные данные. Один явный
|
|
150
|
+
поиск можно разрешить отдельно:
|
|
190
151
|
|
|
191
152
|
```ts
|
|
192
|
-
|
|
193
|
-
|
|
153
|
+
const teacher = await client.resolveTeacher("Иванов И. И.", {
|
|
154
|
+
strategy: "search",
|
|
155
|
+
});
|
|
194
156
|
```
|
|
195
157
|
|
|
196
|
-
|
|
158
|
+
Или заранее загрузить нужные справочники:
|
|
197
159
|
|
|
198
160
|
```ts
|
|
199
|
-
|
|
161
|
+
await client.preloadDirectory({
|
|
162
|
+
teachers: true,
|
|
163
|
+
rooms: true,
|
|
164
|
+
facultyIds: [19],
|
|
165
|
+
});
|
|
200
166
|
```
|
|
201
167
|
|
|
202
|
-
|
|
168
|
+
ID присваивается сокращенному имени только при единственном совпадении.
|
|
169
|
+
|
|
170
|
+
## Постоянный репозиторий
|
|
171
|
+
|
|
172
|
+
TTL-кеш ответов и хранилище канонических ID независимы:
|
|
203
173
|
|
|
204
174
|
```ts
|
|
205
|
-
|
|
206
|
-
|
|
175
|
+
const client = new TimetableClient({
|
|
176
|
+
cache: 15 * 60_000,
|
|
177
|
+
cacheAdapter,
|
|
178
|
+
repositoryAdapter,
|
|
179
|
+
});
|
|
180
|
+
```
|
|
207
181
|
|
|
208
|
-
|
|
209
|
-
|
|
182
|
+
`TimetableRepositoryAdapter` можно реализовать поверх БД, KV-хранилища или
|
|
183
|
+
файлового сервиса. Запись использует ревизии compare-and-set, чтобы параллельные
|
|
184
|
+
процессы не перезаписывали установленные ID.
|
|
210
185
|
|
|
211
|
-
|
|
212
|
-
|
|
186
|
+
```ts
|
|
187
|
+
const snapshot = await client.exportRepository();
|
|
213
188
|
```
|
|
214
189
|
|
|
215
|
-
|
|
190
|
+
Для одного процесса есть память:
|
|
216
191
|
|
|
217
192
|
```ts
|
|
218
193
|
import {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
getWeekNumber,
|
|
222
|
-
Period,
|
|
194
|
+
MemoryTimetableRepositoryAdapter,
|
|
195
|
+
TimetableClient,
|
|
223
196
|
} from "chuvsu-js";
|
|
224
197
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
getWeekNumber({ period: Period.SpringSemester });
|
|
198
|
+
const repositoryAdapter = new MemoryTimetableRepositoryAdapter();
|
|
199
|
+
const client = new TimetableClient({ repositoryAdapter });
|
|
228
200
|
```
|
|
229
201
|
|
|
230
|
-
|
|
202
|
+
## Использование в браузере
|
|
231
203
|
|
|
232
|
-
|
|
204
|
+
`chuvsu-js/browser` не содержит `undici`, сертификатов ЧГУ, `Buffer` и HTML-
|
|
205
|
+
парсера. Браузер может получить снимок с сервера и выполнять запросы локально:
|
|
233
206
|
|
|
234
207
|
```ts
|
|
235
|
-
|
|
236
|
-
await lk.login({ email, password });
|
|
237
|
-
const data = await lk.getPersonalData();
|
|
238
|
-
const groupId = await lk.getGroupId();
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
`cache` работает так же, как в `TtClient`: число задаёт единый TTL в мс, объект — TTL по категориям (`personalData`, `photo`, `groupId`).
|
|
242
|
-
Также поддерживаются `cacheAdapter` и `blobAdapter`.
|
|
208
|
+
import { Schedule, TimetableRepository } from "chuvsu-js/browser";
|
|
243
209
|
|
|
244
|
-
|
|
210
|
+
const repository = new TimetableRepository({ snapshot });
|
|
211
|
+
const schedule = new Schedule(
|
|
212
|
+
repository,
|
|
213
|
+
{ type: "group", group: { id: 8919, name: "КТ-41-24" } },
|
|
214
|
+
2026,
|
|
215
|
+
);
|
|
245
216
|
|
|
246
|
-
|
|
217
|
+
const lessons = schedule.on(new Date(2026, 8, 7));
|
|
218
|
+
```
|
|
247
219
|
|
|
248
|
-
|
|
220
|
+
Сетевые клиенты остаются Node-only: авторизация, cookie, сертификаты и CORS
|
|
221
|
+
портала нельзя надежно перенести в чистый браузерный пакет.
|
|
249
222
|
|
|
250
223
|
```ts
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
SummerSession = 4, // Летняя сессия
|
|
256
|
-
}
|
|
224
|
+
import { TimetableClient } from "chuvsu-js"; // Node + ядро
|
|
225
|
+
import { TimetableClient } from "chuvsu-js/node"; // явно Node
|
|
226
|
+
import { TimetableRepository } from "chuvsu-js/browser";
|
|
227
|
+
import { parseGroupSchedule } from "chuvsu-js/parsers";
|
|
257
228
|
```
|
|
258
229
|
|
|
259
|
-
|
|
230
|
+
## Поиск и метаданные
|
|
260
231
|
|
|
261
232
|
```ts
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
233
|
+
const faculties = await client.getFaculties();
|
|
234
|
+
const groups = await client.getFacultyGroups(facultyId);
|
|
235
|
+
const groupsByName = await client.searchGroups("КТ-41");
|
|
236
|
+
const teachers = await client.searchTeachers("Иванов");
|
|
237
|
+
const rooms = await client.searchRooms("Г-40");
|
|
238
|
+
|
|
239
|
+
const teacherInfo = await client.getTeacherInfo(teacherId);
|
|
240
|
+
const teacherPhoto = await client.getTeacherPhoto(teacherId);
|
|
241
|
+
const roomInfo = await client.getRoomInfo(roomId);
|
|
242
|
+
const roomPhoto = await client.getRoomImage(roomId);
|
|
243
|
+
const buildingPhoto = await client.getRoomBuildingImage(roomId);
|
|
244
|
+
const floorPlan = await client.getRoomFloorPlan(roomId);
|
|
266
245
|
```
|
|
267
246
|
|
|
268
|
-
|
|
247
|
+
Фото преподавателя загружается по уже известному прямому URL без лишнего
|
|
248
|
+
запроса страницы преподавателя.
|
|
249
|
+
|
|
250
|
+
## Личный кабинет
|
|
269
251
|
|
|
270
252
|
```ts
|
|
271
|
-
|
|
272
|
-
number: number; // Номер пары
|
|
273
|
-
start: LessonTime; // Начало { date, hours, minutes }
|
|
274
|
-
end: LessonTime; // Конец { date, hours, minutes }
|
|
275
|
-
subject: string; // Предмет
|
|
276
|
-
type: string; // Тип (лекция, практика, лаб. работа)
|
|
277
|
-
room: string; // Аудитория
|
|
278
|
-
teacher: Teacher; // Преподаватель { name, position?, degree? }
|
|
279
|
-
weeks: WeekRange; // Диапазон недель { from, to }
|
|
280
|
-
subgroup?: number; // Подгруппа
|
|
281
|
-
weekParity?: "even" | "odd"; // Чётность недели
|
|
282
|
-
}
|
|
283
|
-
```
|
|
253
|
+
import { StudentPortalClient } from "chuvsu-js";
|
|
284
254
|
|
|
285
|
-
|
|
255
|
+
const portal = new StudentPortalClient({ cache: 60_000 });
|
|
256
|
+
await portal.login({ email: "student@example.com", password: "password" });
|
|
286
257
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
await tt.login({ email: "...", password: "wrong" });
|
|
292
|
-
} catch (e) {
|
|
293
|
-
if (e instanceof AuthError) {
|
|
294
|
-
console.error("Неверные данные для входа");
|
|
295
|
-
}
|
|
296
|
-
}
|
|
258
|
+
const profile = await portal.getProfile();
|
|
259
|
+
const photo = await portal.getProfilePhoto();
|
|
260
|
+
const groupId = await portal.getTimetableGroupId();
|
|
297
261
|
```
|
|
298
262
|
|
|
299
|
-
##
|
|
263
|
+
## Разработка
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
pnpm build
|
|
267
|
+
pnpm test
|
|
268
|
+
pnpm test:coverage
|
|
269
|
+
pnpm fixtures:test
|
|
270
|
+
```
|
|
300
271
|
|
|
301
|
-
|
|
272
|
+
Подробности архитектуры: [`docs/v5-architecture.md`](docs/v5-architecture.md).
|
|
273
|
+
Границы и честная интерпретация тестов: [`docs/testing.md`](docs/testing.md).
|
package/dist/browser.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./core.js";
|
package/dist/browser.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./core.js";
|
package/dist/common/cache.js
CHANGED
|
@@ -2,7 +2,14 @@ export class Cache {
|
|
|
2
2
|
ttls;
|
|
3
3
|
store = new Map();
|
|
4
4
|
constructor(ttls) {
|
|
5
|
-
|
|
5
|
+
for (const [category, ttl] of Object.entries(ttls)) {
|
|
6
|
+
if (ttl != null &&
|
|
7
|
+
ttl !== Infinity &&
|
|
8
|
+
(!Number.isFinite(ttl) || ttl < 0)) {
|
|
9
|
+
throw new RangeError(`Invalid cache TTL for ${category}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
this.ttls = { ...ttls };
|
|
6
13
|
}
|
|
7
14
|
get(category, key) {
|
|
8
15
|
const ttl = this.ttls[category];
|
|
@@ -15,13 +22,13 @@ export class Cache {
|
|
|
15
22
|
this.store.delete(`${category}:${key}`);
|
|
16
23
|
return null;
|
|
17
24
|
}
|
|
18
|
-
return entry.data;
|
|
25
|
+
return structuredClone(entry.data);
|
|
19
26
|
}
|
|
20
27
|
set(category, key, data) {
|
|
21
28
|
if (this.ttls[category] == null)
|
|
22
29
|
return;
|
|
23
30
|
this.store.set(`${category}:${key}`, {
|
|
24
|
-
data,
|
|
31
|
+
data: structuredClone(data),
|
|
25
32
|
timestamp: Date.now(),
|
|
26
33
|
});
|
|
27
34
|
}
|
|
@@ -38,11 +45,25 @@ export class Cache {
|
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
export() {
|
|
41
|
-
return Object.fromEntries(this.store);
|
|
48
|
+
return structuredClone(Object.fromEntries(this.store));
|
|
42
49
|
}
|
|
43
50
|
import(data) {
|
|
44
51
|
for (const [key, entry] of Object.entries(data)) {
|
|
45
|
-
|
|
52
|
+
const separator = key.indexOf(":");
|
|
53
|
+
const category = separator < 1 || separator === key.length - 1
|
|
54
|
+
? ""
|
|
55
|
+
: key.slice(0, separator);
|
|
56
|
+
if (this.ttls[category] == null) {
|
|
57
|
+
throw new TypeError(`Invalid or disabled cache category in ${key}`);
|
|
58
|
+
}
|
|
59
|
+
if (entry == null ||
|
|
60
|
+
typeof entry !== "object" ||
|
|
61
|
+
!Number.isFinite(entry.timestamp) ||
|
|
62
|
+
entry.timestamp < 0 ||
|
|
63
|
+
!("data" in entry)) {
|
|
64
|
+
throw new TypeError(`Invalid cache entry: ${key}`);
|
|
65
|
+
}
|
|
66
|
+
this.store.set(key, structuredClone(entry));
|
|
46
67
|
}
|
|
47
68
|
}
|
|
48
69
|
}
|
|
@@ -51,8 +72,8 @@ export class HybridCache {
|
|
|
51
72
|
ttls;
|
|
52
73
|
adapter;
|
|
53
74
|
constructor(ttls, adapter) {
|
|
54
|
-
this.ttls = ttls;
|
|
55
|
-
this.memory = new Cache(ttls);
|
|
75
|
+
this.ttls = { ...ttls };
|
|
76
|
+
this.memory = new Cache(this.ttls);
|
|
56
77
|
this.adapter = adapter;
|
|
57
78
|
}
|
|
58
79
|
ttl(category) {
|
|
@@ -75,7 +96,7 @@ export class HybridCache {
|
|
|
75
96
|
if (external === null || external === undefined)
|
|
76
97
|
return null;
|
|
77
98
|
this.memory.set(category, key, external);
|
|
78
|
-
return
|
|
99
|
+
return this.memory.get(category, key);
|
|
79
100
|
}
|
|
80
101
|
async set(category, key, data) {
|
|
81
102
|
this.memory.set(category, key, data);
|
|
@@ -85,7 +106,7 @@ export class HybridCache {
|
|
|
85
106
|
const ttl = this.ttls[category];
|
|
86
107
|
if (ttl == null || !this.adapter)
|
|
87
108
|
return;
|
|
88
|
-
await this.adapter.set(category, key, data, ttl);
|
|
109
|
+
await this.adapter.set(category, key, structuredClone(data), ttl);
|
|
89
110
|
}
|
|
90
111
|
async clear(category) {
|
|
91
112
|
this.memory.clear(category);
|
package/dist/common/http.d.ts
CHANGED
|
@@ -3,11 +3,17 @@ export interface HttpResponse {
|
|
|
3
3
|
body: string;
|
|
4
4
|
location?: string;
|
|
5
5
|
}
|
|
6
|
+
export interface HttpBufferResponse {
|
|
7
|
+
status: number;
|
|
8
|
+
body: Buffer;
|
|
9
|
+
contentType?: string;
|
|
10
|
+
}
|
|
6
11
|
export declare class HttpClient {
|
|
7
12
|
private cookies;
|
|
8
13
|
private cookieHeader;
|
|
9
14
|
private saveCookies;
|
|
10
15
|
get(url: string, followRedirects?: boolean): Promise<HttpResponse>;
|
|
11
16
|
getBuffer(url: string): Promise<Buffer>;
|
|
17
|
+
getBufferResponse(url: string): Promise<HttpBufferResponse>;
|
|
12
18
|
post(url: string, data: Record<string, string>, followRedirects?: boolean): Promise<HttpResponse>;
|
|
13
19
|
}
|
package/dist/common/http.js
CHANGED
|
@@ -33,13 +33,20 @@ export class HttpClient {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
async getBuffer(url) {
|
|
36
|
+
return (await this.getBufferResponse(url)).body;
|
|
37
|
+
}
|
|
38
|
+
async getBufferResponse(url) {
|
|
36
39
|
const res = await fetch(url, {
|
|
37
40
|
method: "GET",
|
|
38
41
|
headers: { Cookie: this.cookieHeader() },
|
|
39
42
|
dispatcher: agent,
|
|
40
43
|
});
|
|
41
44
|
this.saveCookies(res.headers);
|
|
42
|
-
return
|
|
45
|
+
return {
|
|
46
|
+
status: res.status,
|
|
47
|
+
body: Buffer.from(await res.arrayBuffer()),
|
|
48
|
+
contentType: res.headers.get("content-type") ?? undefined,
|
|
49
|
+
};
|
|
43
50
|
}
|
|
44
51
|
async post(url, data, followRedirects = true) {
|
|
45
52
|
const body = new URLSearchParams(data).toString();
|
package/dist/common/parse.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare function text(el: Element | null): string;
|
|
|
4
4
|
/** Parse "HH:MM" into {hours, minutes} */
|
|
5
5
|
export declare function parseTime(s: string): Time;
|
|
6
6
|
/** Parse "2 нед." -> {from:2,to:2}, "6 - 8 нед." -> {from:6,to:8} */
|
|
7
|
-
export declare function parseWeeks(s: string): WeekRange;
|
|
7
|
+
export declare function parseWeeks(s: string): WeekRange | undefined;
|
|
8
8
|
/** Parse <sup>*</sup> / <sup>**</sup> markers: * = odd week, ** = even week */
|
|
9
9
|
export declare function parseWeekParity(html: string): "even" | "odd" | undefined;
|
|
10
10
|
export declare function parseTeacher(s: string): Teacher;
|
package/dist/common/parse.js
CHANGED
|
@@ -18,7 +18,7 @@ export function parseWeeks(s) {
|
|
|
18
18
|
const single = s.match(/(\d+)/);
|
|
19
19
|
if (single)
|
|
20
20
|
return { from: parseInt(single[1]), to: parseInt(single[1]) };
|
|
21
|
-
return
|
|
21
|
+
return undefined;
|
|
22
22
|
}
|
|
23
23
|
/** Parse <sup>*</sup> / <sup>**</sup> markers: * = odd week, ** = even week */
|
|
24
24
|
export function parseWeekParity(html) {
|
|
@@ -33,7 +33,7 @@ export function parseTeacher(s) {
|
|
|
33
33
|
return { name: "" };
|
|
34
34
|
const posMatch = trimmed.match(/^(доц\.|проф\.|ст\.преп\.|ст\. преп\.|преп\.|асс\.|зав\.каф\.)\s*/);
|
|
35
35
|
const afterPos = posMatch ? trimmed.slice(posMatch[0].length) : trimmed;
|
|
36
|
-
const degMatch = afterPos.match(/^([кд]\.[а-яё.-]
|
|
36
|
+
const degMatch = afterPos.match(/^((?:[кд]\.[а-яё.-]+н\.|к\.иск\.))\s*/);
|
|
37
37
|
const name = degMatch ? afterPos.slice(degMatch[0].length).trim() : afterPos.trim();
|
|
38
38
|
const result = { name };
|
|
39
39
|
if (posMatch)
|