diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..2c58f6de9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +packages/sigarra/src/generated/** linguist-generated=true diff --git a/.github/workflows/codeanalysis.yaml b/.github/workflows/codeanalysis.yaml index f678408e8..f573938c0 100644 --- a/.github/workflows/codeanalysis.yaml +++ b/.github/workflows/codeanalysis.yaml @@ -58,5 +58,9 @@ jobs: working-directory: apps/frontend run: bun run typecheck + - name: Typecheck sigarra + working-directory: packages/sigarra + run: bun run typecheck + - name: Lint run: bun run lint diff --git a/.github/workflows/sigarra.yaml b/.github/workflows/sigarra.yaml new file mode 100644 index 000000000..a11fa6d03 --- /dev/null +++ b/.github/workflows/sigarra.yaml @@ -0,0 +1,57 @@ +name: SIGARRA SDK + +on: + pull_request: + branches: + - develop + - main + - beta + paths: + - packages/sigarra/** + - bun.lock + - .github/workflows/sigarra.yaml + +permissions: + contents: read + +jobs: + check-sdk-up-to-date: + runs-on: ubuntu-latest + name: Check if generated SDK is up to date + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.1 + + - name: Cache dependencies + uses: actions/cache@v6 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: bun install --frozen-lockfile + + # Fern runs its TypeScript generator in a Docker container, which the + # GitHub-hosted runner provides. + - name: Generate SDK + working-directory: packages/sigarra + run: bun run generate + + - name: Check for changes + run: | + if [ -n "$(git status --porcelain -- packages/sigarra)" ]; then + git status --short -- packages/sigarra + git diff -- packages/sigarra + echo "::error::The generated SDK is out of date. Run \`bun run generate\` in packages/sigarra and commit the result." + exit 1 + fi diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 6817735cc..cdbe857bd 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -3,5 +3,5 @@ "printWidth": 120, "semi": false, "singleQuote": true, - "ignorePatterns": ["apps/frontend/src/env.d.ts"] + "ignorePatterns": ["apps/frontend/src/env.d.ts", "packages/sigarra/src/generated"] } diff --git a/.oxlintrc.json b/.oxlintrc.json index 84a3d9484..171b258c2 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -6,7 +6,8 @@ ".claude/skills/**", ".cursor/skills/**", ".devin/skills/**", - ".pi/skills/**" + ".pi/skills/**", + "packages/sigarra/src/generated" ], "categories": { "correctness": "error" diff --git a/AGENTS.md b/AGENTS.md index 0ed5f87cc..6660eb054 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ This file provides guidance to AI coding assistants working on this project. **TTS (Time Table Selector)** — a platform where students experiment with class schedule combinations. Developed by [NIAEFEUP](https://ni.fe.up.pt) (FEUP student organization). - **Production**: https://tts.niaefeup.pt -- **Monorepo**: Bun workspace with `apps/backend` and `apps/frontend` +- **Monorepo**: Bun workspace with `apps/backend`, `apps/frontend` and `packages/sigarra` ## Tech Stack @@ -21,6 +21,7 @@ This file provides guidance to AI coding assistants working on this project. | UI primitives | Radix UI, Headless UI | | Validation | Zod + varlock (env vars) | | Formatting/Linting | Oxfmt + Oxlint (NOT Prettier/ESLint) | +| SIGARRA SDK | OpenAPI 3.1 spec + Fern-generated TypeScript client | ## Commands @@ -55,6 +56,12 @@ bun run typecheck # Typecheck only bun run preview # Preview production build ``` +### SIGARRA SDK (`packages/sigarra`) +```bash +bun run typecheck # Typecheck only +bun run generate # Regenerate src/generated from openapi.json (needs Docker) +``` + ### Docker ```bash docker compose up -d --wait # Start PostgreSQL @@ -88,6 +95,12 @@ apps/ ├── styles/ # CSS (Tailwind + custom) ├── utils/ # Utility functions └── config/ # App config (local.json, prod.json) +packages/ +└── sigarra/ + ├── openapi.json # Hand-written OpenAPI spec of SIGARRA (single source of truth) + ├── fern/ # Fern config and SDK-only spec overrides + ├── scripts/ # Patches applied to the generated SDK + └── src/generated/ # Fern-generated TypeScript SDK (committed, do not edit) ``` ### Backend Architecture (Clean Architecture) @@ -216,6 +229,7 @@ cd apps/frontend && bun run dev # Terminal 2 - **Do NOT modify `apps/backend/legacy/`** — deprecated Django code kept for reference only - **Do NOT use Prettier or ESLint** — project uses Oxfmt and Oxlint - **Do NOT follow Prisma 6/7 docs** — this project uses Prisma 8 (contract-first), a full rewrite. Read `apps/backend/docs/schema-migration.md` and load the `prisma-8` skill for guidance +- **Do NOT edit `packages/sigarra/src/generated/` by hand** — it is generated by Fern. Change `openapi.json` (or `scripts/patch-generated.ts`), then run `bun run generate` in `packages/sigarra` and commit the result. CI fails if the committed SDK is out of date - **Do NOT edit `contract.json` or `contract.d.ts` directly** — they are git-ignored and generated. Edit `contract.prisma` then run `bun run contract:emit` - **Do NOT run `db:seed` or `db:seed:demo` casually** — they truncate ALL tables - **Do NOT add business logic to the interface layer** — keep routes thin, logic belongs in application layer diff --git a/README.md b/README.md index 904a1cfdd..653961421 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ they can pick at the start of the semester. Made with ❤️ by [NIAEFEUP](https://ni.fe.up.pt). Production: . -This repository is a **Bun workspace monorepo** with two apps: +This repository is a **Bun workspace monorepo** with two apps and a shared package: -| Path | Stack | Dev URL | -| --------------- | --------------------------- | --------------------- | -| `apps/backend` | Bun + Elysia + Prisma 8 | http://localhost:3000 | -| `apps/frontend` | React + Vite + Tailwind | http://localhost:3100 | +| Path | Stack | Dev URL | +| ------------------ | ----------------------------------------------- | --------------------- | +| `apps/backend` | Bun + Elysia + Prisma 8 | http://localhost:3000 | +| `apps/frontend` | React + Vite + Tailwind | http://localhost:3100 | +| `packages/sigarra` | TypeScript SDK for SIGARRA, generated with Fern | — | > New here? Follow **[Quick start](#quick-start-fresh-clone)** top to bottom. For team workflow > (branching, commits, PRs) read [`.github/CONTRIBUTING.md`](./.github/CONTRIBUTING.md). @@ -24,6 +25,7 @@ This repository is a **Bun workspace monorepo** with two apps: - [Authentication](#authentication) - [Database](#database) - [Docker](#docker) +- [SIGARRA SDK](#sigarra-sdk) - [Quality checks & CI](#quality-checks--ci) - [Useful commands](#useful-commands) - [Project structure](#project-structure) @@ -50,7 +52,7 @@ Every command below is meant to be copy-pasted from the repository root unless a git clone https://github.com/NIAEFEUP/tts-fe.git # or the SSH URL if you prefer cd tts-fe -# 1. Install all workspace dependencies (installs both apps) +# 1. Install all workspace dependencies (installs both apps and the SIGARRA SDK) bun install # 2. Start PostgreSQL (127.0.0.1:5434, user/password/db = tts/tts/tts) @@ -292,6 +294,10 @@ docker build --target backend-prod -t tts-be . docker build --target frontend-prod -t tts-fe . ``` +## SIGARRA SDK + +`packages/sigarra` (`@tts/sigarra`, private) is a TypeScript SDK for [SIGARRA](https://sigarra.up.pt), generated by [Fern](https://buildwithfern.com) from the hand-written OpenAPI spec [`packages/sigarra/openapi.json`](./packages/sigarra/openapi.json). The generated code is committed under `packages/sigarra/src/generated`: never edit it by hand. After changing the spec, regenerate it from `packages/sigarra` with `bun run generate` (needs Docker and `node`). Retries are opt-in (default `0`). See [`packages/sigarra/README.md`](./packages/sigarra/README.md) for details. + ## Quality checks & CI The monorepo uses [Oxlint](https://oxc.rs) for linting and [Oxfmt](https://oxc.rs) for formatting. @@ -299,19 +305,20 @@ A Husky pre-commit hook runs `oxfmt --check` and `oxlint` automatically on every ```bash # from the repository root -bun run lint # lint both apps +bun run lint # lint the workspace (oxlint) bun run lint:fix # lint + auto-fix -bun run format # format both apps +bun run format # format the workspace (apps + SIGARRA package) bun run format:check # check formatting without writing # per app cd apps/backend && bun run test && bun run typecheck # bun test; emits contract then tsc cd apps/frontend && bun run typecheck && bun run build # tsc; needs the emitted contract +cd packages/sigarra && bun run typecheck # tsc --noEmit ``` Before pushing, run the same checks CI runs: `bun run contract:emit` in `apps/backend`, -`bun run typecheck` and `bun run test` there, `bun run typecheck` in `apps/frontend`, and -`bun run lint` from the root. See [`.github/workflows/codeanalysis.yaml`](./.github/workflows/codeanalysis.yaml). +`bun run typecheck` and `bun run test` there, `bun run typecheck` in `apps/frontend` and +`packages/sigarra`, and `bun run lint` from the root. See [`.github/workflows/codeanalysis.yaml`](./.github/workflows/codeanalysis.yaml). ## Useful commands @@ -356,6 +363,10 @@ bun run typecheck # typecheck only bun run preview # preview the production build locally bun run lint # oxlint +# ── SIGARRA SDK (packages/sigarra) ────────────────────────────────────────── +bun run generate # regenerate src/generated from openapi.json (needs Docker) +bun run typecheck # tsc --noEmit + # ── docker ────────────────────────────────────────────────────────────────── docker compose up -d --wait # start PostgreSQL docker compose down # stop (keeps data) @@ -382,6 +393,11 @@ docker build --target frontend-prod -t tts-fe . │ └── frontend/ # React + Vite SPA │ ├── src/ │ └── .env.schema # Frontend env vars + defaults (varlock) +├── packages/ +│ └── sigarra/ # @tts/sigarra — Fern-generated TypeScript SDK +│ ├── openapi.json # Hand-written OpenAPI spec (source of truth) +│ ├── src/generated/ # Generated code — never edit by hand +│ └── scripts/ # Post-generation patches (Headers, retries) ├── docs/ # Cross-cutting docs ├── .github/ # CI workflows, issue/PR templates, CONTRIBUTING ├── docker-compose.yaml # PostgreSQL for local dev @@ -441,4 +457,5 @@ The frontend type-checks against the backend's emitted declarations, so emit the - [`apps/backend/docs/endpoints.md`](./apps/backend/docs/endpoints.md) — **the** API source of truth. - [`apps/backend/docs/tech-stack.md`](./apps/backend/docs/tech-stack.md) — stack decisions and Prisma 8 usage. - [`apps/backend/docs/schema-migration.md`](./apps/backend/docs/schema-migration.md) — legacy Django → Prisma 8 mapping. +- [`packages/sigarra/README.md`](./packages/sigarra/README.md) — SIGARRA SDK generation and retry policy. - [`docs/all-issues-guide.md`](./docs/all-issues-guide.md) — the cutover issue breakdown. diff --git a/bun.lock b/bun.lock index 0a8c0024e..9083236e9 100644 --- a/bun.lock +++ b/bun.lock @@ -118,6 +118,14 @@ "varlock": "^1.21.1", }, }, + "packages/sigarra": { + "name": "@tts/sigarra", + "devDependencies": { + "@types/node": "^25.6.2", + "fern-api": "5.144.3", + "typescript": "^6.0.3", + }, + }, }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], @@ -214,6 +222,24 @@ "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@boundaryml/baml": ["@boundaryml/baml@0.219.0", "", { "dependencies": { "@scarf/scarf": "^1.3.0" }, "optionalDependencies": { "@boundaryml/baml-darwin-arm64": "0.219.0", "@boundaryml/baml-darwin-x64": "0.219.0", "@boundaryml/baml-linux-arm64-gnu": "0.219.0", "@boundaryml/baml-linux-arm64-musl": "0.219.0", "@boundaryml/baml-linux-x64-gnu": "0.219.0", "@boundaryml/baml-linux-x64-musl": "0.219.0", "@boundaryml/baml-win32-arm64-msvc": "0.219.0", "@boundaryml/baml-win32-x64-msvc": "0.219.0" }, "bin": { "baml": "cli.js", "baml-cli": "cli.js" } }, "sha512-hE6t6G/1Td9yYN/T6E13igF06ZHD0J9dr5FH3tjCCZQplqTLBiVn4wyVoboK7ypjdHdGkg6O0vAcXrcbdwM6pA=="], + + "@boundaryml/baml-darwin-arm64": ["@boundaryml/baml-darwin-arm64@0.219.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jhPN83UM+9Y99aCbzIS/3OpC7N8dOukPZO2piueIbzsmpaJrzTW+srKxYtVDE26Q9riFypOmizSs5SBFst04Fw=="], + + "@boundaryml/baml-darwin-x64": ["@boundaryml/baml-darwin-x64@0.219.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-t7pnbVT3KEE3+stOrW7nATdipCWK8OK1gTV8qiD4pN8lZwGMtJpzrVvgWeycroWCaeJ8tNsQA/V2FyFU9pqjkg=="], + + "@boundaryml/baml-linux-arm64-gnu": ["@boundaryml/baml-linux-arm64-gnu@0.219.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-a3ikRhlOdX+Lb08TaZU4k1Qb55mAHMuwArxpM1No/ltc0ylvo9EbJ4Nv0ZKemEHdngM6udagwdDxohpNzR1QTg=="], + + "@boundaryml/baml-linux-arm64-musl": ["@boundaryml/baml-linux-arm64-musl@0.219.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KjwJL5aXf4XvwpX3RwMau19XcHDcffhbiTX+tk41kGC4LZl7UXB4FgnOBtSOMvWtHj2f4cIN81+yCZBRsMxdNg=="], + + "@boundaryml/baml-linux-x64-gnu": ["@boundaryml/baml-linux-x64-gnu@0.219.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JWMhzx1LDfCPwDbOUQbFaPgNb7TUvLCb4+1TjRAJR+Z2Nl08RqNWUUfFctMEzsN6HrcHVOZ0xSl/hJRxN8OcTA=="], + + "@boundaryml/baml-linux-x64-musl": ["@boundaryml/baml-linux-x64-musl@0.219.0", "", { "os": "linux", "cpu": "x64" }, "sha512-lPosn6eJqI+8EjMI71AUEb3B9uk4dUfQgerbDyu6FvNMWx+HZDPB4UWl66+OlEeMA2Gb6DUp3LLE/Jl9/KDKSg=="], + + "@boundaryml/baml-win32-arm64-msvc": ["@boundaryml/baml-win32-arm64-msvc@0.219.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-djy9pn8s1Ogr3ovTDxDcOiM1YOhkBwNsBnlZDQzrIdR/LgSh/GGmrwW5Ks3gfa2NvvNOIFPccdUp0nDZZNsjfQ=="], + + "@boundaryml/baml-win32-x64-msvc": ["@boundaryml/baml-win32-x64-msvc@0.219.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BJkgq/qyn3BcBZt3iyfYWirZyZrKiqa1J0OTWd45P8OQ8dJhRnU6JGcpXPTXNmkryXQFY+O4HA+lQBKA5SZ7kg=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], @@ -792,6 +818,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.64.0", "", { "os": "win32", "cpu": "x64" }, "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg=="], + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@8.55.2", "", { "dependencies": { "@sentry/core": "8.55.2" } }, "sha512-GnKod+gL/Y+1FUM/RGV8q6le1CoyiGbT40MitEK7eVwWe+bfTRq1gN7ioupyHFMUg1RlQkDQ4/sENmio/uow5A=="], @@ -910,6 +938,8 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tts/sigarra": ["@tts/sigarra@workspace:packages/sigarra"], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/aws-lambda": ["@types/aws-lambda@8.10.164", "", {}, "sha512-XOnrazWcOd6yWPnR7DxqCBPPYciDjHq+NN8LPwwqBCJNXLyPJgKo47fZpJ113+oyAn9Zti0Cf6H4k6yPcE3WFg=="], @@ -1176,6 +1206,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fern-api": ["fern-api@5.144.3", "", { "optionalDependencies": { "@boundaryml/baml": "^0.219.0" }, "bin": { "fern": "cli.cjs" } }, "sha512-UKT/rQkAXJ7b694P8Y5oeV1JdL1gdQ7SxYogRqLYb6jHFYOAOOzhEtDG4TK5KlrkJtb9NiaT1Q0yNX3fLJMPVA=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], diff --git a/package.json b/package.json index 7df0c122e..8f9dd31a2 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,15 @@ "name": "tts-monorepo", "private": true, "workspaces": [ - "apps/*" + "apps/*", + "packages/*" ], "scripts": { "prepare": "husky", "lint": "oxlint .", "lint:fix": "oxlint . --fix", - "format": "oxfmt apps/backend/src apps/backend/tests apps/frontend/src", - "format:check": "oxfmt --check apps/backend/src apps/backend/tests apps/frontend/src", + "format": "oxfmt apps/backend/src apps/backend/tests apps/frontend/src packages/sigarra/src/index.ts packages/sigarra/scripts", + "format:check": "oxfmt --check apps/backend/src apps/backend/tests apps/frontend/src packages/sigarra/src/index.ts packages/sigarra/scripts", "postinstall": "prisma skills sync || exit 0" }, "devDependencies": { diff --git a/packages/sigarra/.gitignore b/packages/sigarra/.gitignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/packages/sigarra/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/sigarra/README.md b/packages/sigarra/README.md new file mode 100644 index 000000000..afe17c5b5 --- /dev/null +++ b/packages/sigarra/README.md @@ -0,0 +1,53 @@ +# @tts/sigarra + +TypeScript SDK for [SIGARRA](https://sigarra.up.pt), generated from [`openapi.json`](./openapi.json) with [Fern](https://buildwithfern.com). + +```ts +import { SigarraClient } from '@tts/sigarra' + +const sigarra = new SigarraClient() +await sigarra.home.getHomePage({ faculty: 'feup', language: 'pt' }) +``` + +## Authentication + +The SDK doesn't manage sessions. Log in, take the `SI_SESSION` and `SI_SECURITY` cookies from the response, and pass them to a new client through the `headers` option: + +```ts +import { SigarraClient } from '@tts/sigarra' + +const { data, rawResponse } = await new SigarraClient().auth + .login({ faculty: 'feup', language: 'pt', pv_login: username, pv_password: password }) + .withRawResponse() + +const cookie = rawResponse.headers + .getSetCookie() + .map((setCookie) => setCookie.split(';')[0]) + .join('; ') + +const sigarra = new SigarraClient({ headers: { Cookie: cookie } }) +await sigarra.student.getProfile({ faculty: 'feup', language: 'pt', pv_codigo: data.codigo }) +``` + +A wrong username or password throws `Sigarra.ForbiddenError`. Sessions last about 2 hours and are tied to the IP address that logged in; endpoints that also accept them from other addresses say so in their description. Use the SDK from a server, and log in again once the session expires. See the `info.description` of [`openapi.json`](./openapi.json) for the other ways to log in (OpenID Connect and SAML). + +## Generating + +The generated SDK is committed under `src/generated`. Regenerate the SDK with: + +```sh +bun run generate +``` + +This runs `fern generate --local` and then `scripts/patch-generated.ts`, which patches the generated output: it fixes a type error in Fern's generated `Headers.ts` and turns off implicit retries (see [Retries](#retries)). CI regenerates the SDK on pull requests that touch this package and fails if the committed files differ. Fern needs Docker (the Fern TypeScript generator runs in a container) and `node`. With Podman, unqualified image names such as `fernapi/fern-typescript-sdk` must resolve to Docker Hub; if they don't, point `CONTAINERS_REGISTRIES_CONF` to a file containing `unqualified-search-registries = ["docker.io"]`. + +## Retries + +Retries are opt-in. Fern retries every request twice by default, including `POST`s, which could replay non-idempotent calls such as logging in or `finances.generateMbReference`. `scripts/patch-generated.ts` changes that default to 0, so a request is never retried unless you ask for it with `maxRetries`, either for the whole client or for a single request: + +```ts +const sigarra = new SigarraClient({ maxRetries: 2 }) +await sigarra.home.getHomePage({ faculty: 'feup', language: 'pt' }, { maxRetries: 1 }) +``` + +Only enable retries for idempotent requests. Don't edit the generated files by hand: change the patch script instead, and `bun run generate` will reapply it. diff --git a/packages/sigarra/fern/fern.config.json b/packages/sigarra/fern/fern.config.json new file mode 100644 index 000000000..109cdc6be --- /dev/null +++ b/packages/sigarra/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "niaefeup", + "version": "5.144.3" +} diff --git a/packages/sigarra/fern/generators.yml b/packages/sigarra/fern/generators.yml new file mode 100644 index 000000000..cd711fe9c --- /dev/null +++ b/packages/sigarra/fern/generators.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +# +# Output paths are relative to this file. +api: + specs: + - openapi: ../openapi.json + overrides: ./openapi-overrides.yml +default-group: typescript-sdk +groups: + typescript-sdk: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.98.4 + output: + location: local-file-system + path: ../src/generated + config: + namespaceExport: Sigarra diff --git a/packages/sigarra/fern/openapi-overrides.yml b/packages/sigarra/fern/openapi-overrides.yml new file mode 100644 index 000000000..34cdfd6dd --- /dev/null +++ b/packages/sigarra/fern/openapi-overrides.yml @@ -0,0 +1,39 @@ +# Fern-only adjustments; openapi.json stays the accurate description of SIGARRA. +# +# Fern can't generate cookie API keys (SI_SESSION / SI_SECURITY): it ignores them +# and makes every endpoint require the only scheme left, uportoOidc. So, for the +# SDK, drop authentication altogether (pass the session cookies through the +# client's `headers` option) and give auth.oidcToken its token as a plain header. +# +# Endpoints whose session is optional declare `security` explicitly in +# openapi.json, so they must be cleared here as well. +security: [] +components: + securitySchemes: + uportoOidc: null +paths: + /auth/oidc/token: + get: + security: [] + parameters: + - name: Authorization + in: header + required: true + description: "`Bearer `." + schema: + type: string + /{faculty}/{language}/web_page.inicial: + get: + security: [] + /{faculty}/{language}/func_geral.formview: + get: + security: [] + /{faculty}/{language}/fotografias_service.foto: + get: + security: [] + /{faculty}/{language}/vld_validacao.sair: + get: + security: [] + /{faculty}/{language}/ucurr_geral.ficha_uc_view: + get: + security: [] diff --git a/packages/sigarra/openapi.json b/packages/sigarra/openapi.json new file mode 100644 index 000000000..d725cb2ba --- /dev/null +++ b/packages/sigarra/openapi.json @@ -0,0 +1,4991 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "SIGARRA", + "description": "SIGARRA, the academic information system of the University of Porto.\n\n## Authentication\n\nAuthenticated requests need two session cookies, `SI_SESSION` and `SI_SECURITY` (both are required). `HTTP_SESSION`, which HTML pages set when a request has none, has no effect on authentication or on responses. Sessions are bound to the IP address that created them (the one that logged in, or that called `/auth/oidc/token`): from any other address, requests are treated as unauthenticated, although the session keeps working from the original address. The calendar API (`/calendarios-api/`) is the exception: it accepts a session from any address. Sessions expire and there is no refresh endpoint: authenticate again to get new cookies. `GET /{faculty}/{language}/vld_validacao.sair` ends a session. There are three ways to get them:\n\n1. **Credentials.** `POST /{faculty}/{language}/vld_validacao.validacao` (the website's login form) with `p_user` and `p_pass` sets the cookies; `mob_val_geral.autentica` is a JSON alternative. Any faculty path works, including `up`. The cookies have no expiry date, but the session itself ends about 2 hours after logging in, regardless of activity (observed once; it matches the 2 hours of OIDC sessions).\n2. **OpenID Connect.** Get a token from the U.Porto identity provider, a Keycloak realm at `https://open-id.up.pt/realms/sigarra` (discovery: `/.well-known/openid-configuration`), then exchange it for the cookies at `GET /auth/oidc/token`. Each exchange creates a new session that lasts 2 hours, regardless of the token's own lifetime. Apps need their own client registered in that realm (redirect URIs are checked); [uni](https://github.com/NIAEFEUP/uni)'s tokens have the audiences `it-sigarra-oidc-bridge` and `catalogo-apis`. Access tokens last 15 minutes; with the `offline_access` scope, the refresh token does not expire. The password grant rejects U.Porto credentials (`invalid_grant`), so tokens have to be obtained through a browser (authorization code flow, with PKCE for public clients). The `uporto_data` scope adds U.Porto claims to the token and user info: `nmec` (student number, without `up`) and `ous` (faculty acronyms, e.g. `[\"FEUP\"]`).\n3. **SAML (browser only).** `GET /{faculty}/{language}/vld_validacao.federate_login` redirects to U.Porto's Shibboleth identity provider at `wayf.up.pt`; SIGARRA is the service provider `https://sigarra.up.pt/shibboleth`.\n\nWithout a valid session, JSON endpoints answer `403` with `{\"erro\", \"erro_msg\"}`, HTML pages answer `403` with an error page, and the calendar API answers `403` with an empty body. Some pages answer `200` with an error page instead; this is noted on each of them.\n\n## Other conventions\n\n- Pages under `/{faculty}/{language}/` are PL/SQL procedures: an unknown or missing parameter makes SIGARRA answer `404`.\n- Many JSON endpoints are served with an inaccurate content type; they are noted individually.", + "version": "0.1.0" + }, + "servers": [ + { + "url": "https://sigarra.up.pt" + } + ], + "tags": [ + { + "name": "auth", + "description": "Logging in (credentials, OpenID Connect, SAML) and out; obtains the session cookies." + }, + { + "name": "calendar", + "description": "Calendar API (`/calendarios-api/`): schedule events and tables of students, course units and professors." + }, + { + "name": "canteen", + "description": "Canteen menus." + }, + { + "name": "course", + "description": "Degree programme pages: study plan and class vacancies." + }, + { + "name": "courseUnit", + "description": "Course units (UCs): information, classes, enrolled students, files, occurrences, schedules and exams." + }, + { + "name": "finances", + "description": "Current account, printing balance and Multibanco payment references." + }, + { + "name": "home", + "description": "Faculty home page and the list of faculties." + }, + { + "name": "parking", + "description": "Parking lot occupancy." + }, + { + "name": "people", + "description": "People-related resources, such as photographs." + }, + { + "name": "professor", + "description": "Professor pages and schedules." + }, + { + "name": "student", + "description": "Student schedule, exams, academic path, profile, courses and enrolments." + }, + { + "name": "summaries", + "description": "Class summaries: classes of a course unit, their lessons and lesson summaries." + } + ], + "paths": { + "/{faculty}/{language}/mob_hor_geral.estudante": { + "get": { + "operationId": "student-get-schedule", + "tags": [ + "student" + ], + "summary": "Student schedule", + "description": "Weekly timetable of a student. Returns the weekly timetable of the week that contains `pv_semana_ini`; `pv_semana_fim` does not widen the range.", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getSchedule", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_codigo", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pv_semana_ini", + "in": "query", + "schema": { + "type": "string" + }, + "required": true, + "description": "Start of the period, as YYYYMMDD." + }, + { + "name": "pv_semana_fim", + "in": "query", + "schema": { + "type": "string" + }, + "required": true, + "description": "End of the period, as YYYYMMDD." + } + ], + "deprecated": false, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WeeklySchedule" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/{faculty}/{language}/mob_fest_geral.exames": { + "get": { + "operationId": "student-get-exams", + "tags": [ + "student" + ], + "summary": "Exams", + "description": "The authenticated user's exams.", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getExams", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "name": "pv_codigo", + "in": "query", + "description": "Student number.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "ocorr_id": { + "type": "integer", + "description": "Occurrence ID." + }, + "ocorr_nome": { + "type": "string", + "description": "Occurrence name." + }, + "data": { + "type": "string", + "description": "ISO 8601" + }, + "hora_inicio": { + "type": "string", + "description": "ISO 8601" + }, + "hora_fim": { + "type": "string", + "description": "ISO 8601" + }, + "duracao": {}, + "pfunc_id": { + "type": "integer", + "description": "Invigilator." + }, + "tipo": { + "type": "string", + "description": "Exam type." + }, + "tipo_descricao": { + "type": "string", + "description": "Description of the exam type." + }, + "epoca": { + "type": "string", + "description": "Name of the exam season." + }, + "salas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "espaco_id": { + "type": "integer" + }, + "espaco_sigla": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "deprecated": false + } + }, + "/{faculty}/{language}/mob_fest_geral.percurso_academico": { + "get": { + "operationId": "student-get-academic-path", + "tags": [ + "student" + ], + "summary": "Academic path", + "description": "The authenticated user's academic path.", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getAcademicPath", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_codigo", + "in": "query", + "description": "Student number.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo_min", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo_max", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fest_id": { + "type": "integer" + }, + "fest_tipo": { + "type": "string" + }, + "fest_tipo_descr": { + "type": "string" + }, + "cur_nome": { + "type": "string" + }, + "cur_name": { + "type": "string" + }, + "cur_sigla": { + "type": "string" + }, + "ano_curricular": { + "type": "string" + }, + "media": { + "type": "number" + }, + "inscricoes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ucurr_id": { + "type": "integer" + }, + "ucurr_codigo": { + "type": "string" + }, + "ucurr_sigla": { + "type": "string" + }, + "ucurr_nome": { + "type": "string" + }, + "ucurr_name": { + "type": "string" + }, + "a_lectivo": { + "type": "integer" + }, + "ano": { + "type": "integer" + }, + "ocorr_id": { + "type": "integer" + }, + "ucurr_tipo": { + "type": "string" + }, + "ucurr_duracao_tipo": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "inst_id": { + "type": "integer" + }, + "comp_id": { + "type": "integer" + }, + "per_id": { + "type": "integer" + }, + "per_codigo": { + "type": "string" + }, + "per_nome": { + "type": "string" + }, + "tipo": { + "type": "string" + }, + "estado": { + "type": "string" + }, + "frequencia": { + "type": "string" + }, + "avaliacao": { + "type": "string" + }, + "resultado_melhor": { + "type": "string" + }, + "resultado_ects": { + "type": "string" + }, + "aval_a_lectivo": { + "type": [ + "integer", + "null" + ] + }, + "resultado_insc": { + "type": "string" + }, + "creditos_ucn": { + "type": "null" + }, + "creditos_ects": { + "type": "number" + }, + "n_ucurrs": { + "type": "integer" + }, + "tipo_ucurrs": { + "type": "string" + }, + "obr_opt": { + "type": "string" + }, + "regra_eq": { + "type": "string" + }, + "devedor": { + "type": "string" + }, + "incumpridor": { + "type": "string" + } + }, + "required": [ + "ucurr_id", + "ucurr_codigo", + "ucurr_sigla", + "ucurr_nome", + "ucurr_name", + "a_lectivo", + "ano", + "ocorr_id", + "ucurr_tipo", + "ucurr_duracao_tipo", + "id", + "inst_id", + "comp_id", + "per_id", + "per_codigo", + "per_nome", + "tipo", + "estado", + "frequencia", + "avaliacao", + "resultado_melhor", + "resultado_ects", + "aval_a_lectivo", + "resultado_insc", + "creditos_ucn", + "creditos_ects", + "n_ucurrs", + "tipo_ucurrs", + "obr_opt", + "regra_eq", + "devedor", + "incumpridor" + ] + } + } + }, + "required": [ + "fest_id", + "fest_tipo", + "fest_tipo_descr", + "cur_nome", + "cur_name", + "cur_sigla", + "ano_curricular", + "media", + "inscricoes" + ] + } + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "deprecated": false + } + }, + "/{faculty}/{language}/mob_fest_geral.perfil": { + "get": { + "operationId": "student-get-profile", + "tags": [ + "student" + ], + "summary": "Profile", + "description": "The authenticated user's details.", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getProfile", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_codigo", + "in": "query", + "description": "Student number.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "codigo": { + "type": "string" + }, + "nome": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_alt": { + "type": "string" + }, + "pagina_web": { + "type": "string" + }, + "telemovel": { + "type": "string" + }, + "telefone": { + "type": "string" + }, + "cursos": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fest_id": { + "type": "integer" + }, + "fest_tipo": { + "type": "string" + }, + "fest_tipo_descr": { + "type": "string" + }, + "cur_nome": { + "type": "string" + }, + "cur_name": { + "type": "string" + }, + "cur_sigla": { + "type": "string" + }, + "fest_a_lect_1_insc": { + "type": "integer" + }, + "fest_d_1_insc": { + "type": "string" + }, + "cur_id": { + "type": "integer" + }, + "est_alectivo": { + "type": "integer" + }, + "est_sig": { + "type": "string" + }, + "est_nome": { + "type": "string" + }, + "est_d_inicio": { + "type": "string" + }, + "est_d_fim": { + "type": "string" + }, + "inst_nome": { + "type": "string" + }, + "inst_sigla": { + "type": "string" + }, + "ano_curricular": { + "type": "string" + } + }, + "required": [ + "fest_id", + "fest_tipo", + "fest_tipo_descr", + "cur_nome", + "cur_name", + "cur_sigla", + "fest_a_lect_1_insc", + "fest_d_1_insc", + "cur_id", + "est_alectivo", + "est_sig", + "est_nome", + "est_d_inicio", + "est_d_fim", + "inst_nome", + "inst_sigla", + "ano_curricular" + ] + } + } + }, + "required": [ + "codigo", + "nome", + "email", + "email_alt", + "pagina_web", + "telemovel", + "telefone", + "cursos" + ] + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "deprecated": false + } + }, + "/{faculty}/{language}/mob_fest_geral.ucurr_inscricoes_corrente": { + "get": { + "operationId": "student-get-current-courses", + "tags": [ + "student" + ], + "summary": "Courses and their course units", + "description": "Lists the courses the student is enrolled in, with their current course units.", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getCurrentCourses", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_codigo", + "in": "query", + "description": "Student number.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fest_id": { + "type": "integer" + }, + "fest_tipo": { + "type": "string" + }, + "fest_tipo_descr": { + "type": "string" + }, + "cur_nome": { + "type": "string" + }, + "cur_name": { + "type": "string" + }, + "cur_sigla": { + "type": "string" + }, + "ano_curricular": { + "type": "string" + }, + "inscricoes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ucurr_id": { + "type": "integer" + }, + "ucurr_codigo": { + "type": "string" + }, + "ucurr_sigla": { + "type": "string" + }, + "ucurr_nome": { + "type": "string" + }, + "ucurr_name": { + "type": "string" + }, + "a_lectivo": { + "type": "integer" + }, + "ano": { + "type": "integer" + }, + "ocorr_id": { + "type": "integer" + }, + "ucurr_tipo": { + "type": "string" + }, + "ucurr_duracao_tipo": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "inst_id": { + "type": "integer" + }, + "comp_id": { + "type": "integer" + }, + "per_id": { + "type": "integer" + }, + "per_codigo": { + "type": "string" + }, + "per_nome": { + "type": "string" + }, + "tipo": { + "type": "string" + }, + "estado": { + "type": "string" + }, + "frequencia": { + "type": "string" + }, + "avaliacao": { + "type": "string" + }, + "resultado_melhor": { + "type": "string" + }, + "resultado_ects": { + "type": "string" + }, + "aval_a_lectivo": { + "type": [ + "integer", + "null" + ] + }, + "resultado_insc": { + "type": "string" + }, + "creditos_ucn": { + "type": "null" + }, + "creditos_ects": { + "type": "number" + }, + "n_ucurrs": { + "type": "integer" + }, + "tipo_ucurrs": { + "type": "string" + }, + "obr_opt": { + "type": "string" + }, + "regra_eq": { + "type": "string" + }, + "devedor": { + "type": "string" + }, + "incumpridor": { + "type": "string" + } + }, + "required": [ + "ucurr_id", + "ucurr_codigo", + "ucurr_sigla", + "ucurr_nome", + "ucurr_name", + "a_lectivo", + "ano", + "ocorr_id", + "ucurr_tipo", + "ucurr_duracao_tipo", + "id", + "inst_id", + "comp_id", + "per_id", + "per_codigo", + "per_nome", + "tipo", + "estado", + "frequencia", + "avaliacao", + "resultado_melhor", + "resultado_ects", + "aval_a_lectivo", + "resultado_insc", + "creditos_ucn", + "creditos_ects", + "n_ucurrs", + "tipo_ucurrs", + "obr_opt", + "regra_eq", + "devedor", + "incumpridor" + ] + } + } + }, + "required": [ + "fest_id", + "fest_tipo", + "fest_tipo_descr", + "cur_nome", + "cur_name", + "cur_sigla", + "ano_curricular", + "inscricoes" + ] + } + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "deprecated": false + } + }, + "/{faculty}/{language}/mob_ucurr_geral.outras_ocorrencias": { + "get": { + "operationId": "course-unit-get-occurrences", + "tags": [ + "courseUnit" + ], + "summary": "Course unit occurrences", + "description": "Lists the occurrences of a course unit, given one of its occurrences.", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getOccurrences", + "parameters": [ + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "484377" + }, + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "nome": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ano_letivo": { + "type": "integer" + }, + "data_inicio": { + "type": "string" + }, + "data_fim": { + "type": "string" + }, + "periodo_nome": { + "type": "string" + }, + "periodo_id": { + "type": "string" + } + }, + "required": [ + "id", + "nome", + "name", + "ano_letivo", + "data_inicio", + "data_fim", + "periodo_nome", + "periodo_id" + ] + } + } + } + } + } + }, + "deprecated": false, + "security": [] + } + }, + "/{faculty}/{language}/mob_ucurr_geral.perfil": { + "get": { + "operationId": "course-unit-get-info", + "tags": [ + "courseUnit" + ], + "summary": "Course unit information", + "description": "Given an occurrence ID, returns the contents of its course unit.", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getInfo", + "parameters": [ + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "484377" + }, + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "codigo": { + "type": "string" + }, + "ucurr_id": { + "type": "integer" + }, + "nome": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sigla": { + "type": "string" + }, + "areas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "nome": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "nome", + "name" + ] + } + }, + "ano_lectivo": { + "type": "integer" + }, + "pagina_elearning": { + "type": "string" + }, + "carga_horaria": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tipo": { + "type": "string" + }, + "descricao": { + "type": "string" + }, + "horas": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "tipo", + "descricao", + "horas" + ] + } + }, + "ds": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tipo": { + "type": "string" + }, + "tipo_descricao": { + "type": "string" + }, + "num_turmas": { + "type": "integer" + }, + "num_horas": { + "type": [ + "integer", + "null" + ] + }, + "docentes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "doc_codigo": { + "type": "string" + }, + "nome": { + "type": "string" + }, + "horas": { + "type": "number" + } + }, + "required": [ + "doc_codigo", + "nome", + "horas" + ] + } + } + }, + "required": [ + "tipo", + "tipo_descricao", + "num_turmas", + "num_horas", + "docentes" + ] + } + }, + "responsabilidades": { + "type": "array", + "items": { + "type": "object", + "properties": { + "codigo": { + "type": "string" + }, + "nome": { + "type": "string" + }, + "papel": { + "type": "string" + } + }, + "required": [ + "codigo", + "nome", + "papel" + ] + } + }, + "estado": { + "type": "string" + }, + "lingua": { + "type": "string" + }, + "objectivos": { + "type": "string" + }, + "conteudo": { + "type": "string" + }, + "bibliografia": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tipo": { + "type": "string" + }, + "tipo_descr": { + "type": "string" + }, + "autores": { + "type": "string" + }, + "titulo": { + "type": "string" + }, + "link": { + "type": "string" + }, + "editor": { + "type": "string" + }, + "ano": { + "type": "string" + }, + "isbn": { + "type": "string" + }, + "obs": { + "type": "string" + } + }, + "required": [ + "tipo", + "tipo_descr", + "autores", + "titulo", + "link", + "editor", + "ano", + "isbn", + "obs" + ] + } + }, + "bib_obs": { + "type": "string" + }, + "metodologia": { + "type": "string" + }, + "avaliacao": { + "type": "string" + }, + "software": { + "type": "array", + "items": { + "type": "object", + "properties": { + "nome": { + "type": "null" + }, + "descricao": { + "type": "string" + } + }, + "required": [ + "nome", + "descricao" + ] + } + }, + "keywords": { + "type": "array", + "items": { + "type": "object", + "properties": { + "descricao": { + "type": "string" + } + }, + "required": [ + "descricao" + ] + } + }, + "modo_avaliacao": { + "type": "string" + }, + "comp_avaliacao": { + "type": "array", + "items": { + "type": "object", + "properties": { + "descricao": { + "type": "string" + }, + "tipo": { + "type": "string" + }, + "tipo_descr": { + "type": "string" + }, + "duracao": { + "type": [ + "integer", + "null" + ] + }, + "data_conclusao": { + "type": "string" + }, + "peso": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "descricao", + "tipo", + "tipo_descr", + "duracao", + "data_conclusao", + "peso" + ] + } + }, + "cond_frequencia": { + "type": "string" + }, + "for_avaliacao": { + "type": "string" + }, + "provas_avaliacao": { + "type": "string" + }, + "forma_avaliacao": { + "type": "string" + }, + "forma_melhoria": { + "type": "string" + }, + "observacoes": { + "type": "string" + }, + "pagina_web": { + "type": "string" + } + }, + "required": [ + "codigo", + "ucurr_id", + "nome", + "name", + "sigla", + "areas", + "ano_lectivo", + "pagina_elearning", + "carga_horaria", + "ds", + "responsabilidades" + ] + } + } + } + } + }, + "deprecated": false, + "security": [] + } + }, + "/{faculty}/{language}/mob_ucurr_geral.uc_inscritos": { + "get": { + "operationId": "course-unit-get-enrolled", + "tags": [ + "courseUnit" + ], + "summary": "Course unit enrolled students", + "description": "Lists the people enrolled in a course unit.", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getEnrolled", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "484377" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "codigo": { + "type": "string" + }, + "nome": { + "type": "string" + } + }, + "required": [ + "codigo", + "nome" + ] + } + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "deprecated": false + } + }, + "/{faculty}/{language}/mob_ucurr_geral.uc_turmas": { + "get": { + "operationId": "course-unit-get-classes", + "tags": [ + "courseUnit" + ], + "summary": "Course unit classes", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getClasses", + "parameters": [ + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "484377" + }, + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "turmas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "turma_id": { + "type": "integer" + }, + "n_max_alunos": { + "type": "null" + } + }, + "required": [ + "turma_id", + "n_max_alunos" + ] + } + } + }, + "required": [ + "turmas" + ] + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "deprecated": false, + "description": "For some occurrences (e.g. 2nd-semester ones), SIGARRA answers 200 with a malformed body: a leaked `Status: 404` CGI header followed by `{\"erro\", \"erro_msg\"}`." + } + }, + "/{faculty}/{language}/mob_ucurr_geral.conteudos": { + "get": { + "operationId": "course-unit-get-files", + "tags": [ + "courseUnit" + ], + "summary": "Course unit files", + "description": "Files (contents) of a course unit.", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getFiles", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_ocorrencia_id", + "in": "query", + "description": "ID of the occurrence of the course unit whose files to get.", + "required": true, + "schema": { + "type": "string" + }, + "example": "484400" + } + ], + "deprecated": false, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "codigo": { + "type": "integer" + }, + "nome": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nivel": { + "type": "integer" + }, + "ficheiros": { + "type": "array", + "items": { + "type": "object", + "properties": { + "codigo": { + "type": "integer" + }, + "nome": { + "type": "string" + }, + "tipo": { + "type": "string" + }, + "url": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "tamanho": { + "type": "integer" + }, + "data_actualizacao": { + "type": "string" + }, + "comentario": { + "type": "string" + }, + "descricao": { + "type": "string" + } + }, + "required": [ + "codigo", + "nome", + "tipo", + "url", + "filename", + "tamanho", + "data_actualizacao", + "comentario", + "descricao" + ] + } + } + }, + "required": [ + "codigo", + "nome", + "name", + "nivel", + "ficheiros" + ] + } + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/{faculty}/{language}/conteudos_service.conteudos_cont": { + "get": { + "operationId": "course-unit-download-file", + "tags": [ + "courseUnit" + ], + "summary": "Download file", + "description": "Without a valid session, SIGARRA answers `200` with an HTML error page (\"Erro!\") instead of the file.\n\nDownloads a file by its ID.", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "downloadFile", + "parameters": [ + { + "name": "pct_id", + "in": "query", + "description": "The id of the document to be downloaded", + "required": true, + "schema": { + "type": "string" + }, + "example": "515516" + }, + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "deprecated": false, + "responses": { + "200": { + "description": "The file, with its own content type (e.g. `application/pdf`).", + "headers": { + "Content-Disposition": { + "description": "Contains the file name.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/{faculty}/{language}/mob_ucurr_geral.sum_turmas": { + "get": { + "operationId": "summaries-get-classes", + "tags": [ + "summaries" + ], + "summary": "Classes of a course unit", + "description": "Lists the classes of a course unit occurrence.", + "x-fern-sdk-group-name": "summaries", + "x-fern-sdk-method-name": "getClasses", + "parameters": [ + { + "name": "pv_ocorrencia_id", + "in": "query", + "description": "Course unit occurrence ID.", + "required": true, + "schema": { + "type": "string" + }, + "example": "484400" + }, + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "deprecated": false, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "turmas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "turma_id": { + "type": "integer" + }, + "tipo": { + "type": "string" + }, + "sigla": { + "type": "string" + } + }, + "required": [ + "turma_id", + "tipo", + "sigla" + ] + } + } + }, + "required": [ + "turmas" + ] + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/{faculty}/{language}/mob_ucurr_geral.sum_aulas": { + "get": { + "operationId": "summaries-get-lessons", + "tags": [ + "summaries" + ], + "summary": "Lessons of a class", + "description": "Lists all the lessons of a class, with the ID of each lesson's summary.", + "x-fern-sdk-group-name": "summaries", + "x-fern-sdk-method-name": "getLessons", + "parameters": [ + { + "name": "pv_ocorrencia_id", + "in": "query", + "description": "Course unit occurrence ID.", + "required": true, + "schema": { + "type": "string" + }, + "example": "484400" + }, + { + "name": "pv_turma_id", + "in": "query", + "description": "Class ID.", + "required": true, + "schema": { + "type": "string" + }, + "example": "824506" + }, + { + "name": "pv_tipo_aula", + "in": "query", + "description": "Lesson type.", + "required": true, + "schema": { + "type": "string" + }, + "example": "TP" + }, + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "deprecated": false, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "n_aula": { + "type": "integer" + }, + "cod_sumario": { + "type": "integer" + }, + "data_prevista": { + "type": "string" + }, + "data_efectiva": { + "type": "string" + } + }, + "required": [ + "n_aula", + "cod_sumario", + "data_prevista", + "data_efectiva" + ] + } + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/{faculty}/{language}/mob_ucurr_geral.sumario": { + "get": { + "operationId": "summaries-get-summary", + "tags": [ + "summaries" + ], + "summary": "Lesson summary", + "description": "Gets the summary of a specific lesson.", + "x-fern-sdk-group-name": "summaries", + "x-fern-sdk-method-name": "getSummary", + "parameters": [ + { + "name": "pv_sumario_id", + "in": "query", + "description": "Summary ID.", + "required": true, + "schema": { + "type": "string" + }, + "example": "2146359" + }, + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "deprecated": false, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "n_aula": { + "type": "integer" + }, + "cod_sumario": { + "type": [ + "integer", + "null" + ] + }, + "data_prevista": { + "type": "string" + }, + "data_efectiva": { + "type": "string" + }, + "obs": { + "type": "string" + }, + "sumario": { + "type": "string" + }, + "doc_codigo": { + "type": "string" + }, + "n_alunos": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "n_aula", + "cod_sumario", + "data_prevista", + "data_efectiva", + "obs", + "sumario", + "doc_codigo", + "n_alunos" + ] + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/{faculty}/{language}/instalacs_geral.ocupacao_parques": { + "get": { + "operationId": "parking-get-occupancy", + "tags": [ + "parking" + ], + "summary": "Parking lot occupancy", + "x-fern-sdk-group-name": "parking", + "x-fern-sdk-method-name": "getOccupancy", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "deprecated": false, + "responses": { + "200": { + "description": "OK. JSON, despite being served as `text/html`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "itdc": { + "type": "array", + "items": { + "type": "object", + "properties": { + "versao": { + "type": "string" + }, + "dataresposta": { + "type": "string" + }, + "codigoresposta": { + "type": "string" + }, + "statementsql": { + "type": "string" + }, + "linhas": { + "type": "string" + }, + "erro": { + "type": "string" + }, + "resposta": { + "type": "object", + "properties": { + "p1lotacao": { + "type": "integer" + }, + "p3lotacao": { + "type": "integer" + }, + "p4lotacao": { + "type": "integer" + }, + "data": { + "type": "string" + }, + "P1in": { + "type": "integer" + }, + "P1out": { + "type": "integer" + }, + "P3in": { + "type": "integer" + }, + "P3out": { + "type": "integer" + }, + "P4in": { + "type": "integer" + }, + "P4out": { + "type": "integer" + }, + "p1ocupados": { + "type": "integer" + }, + "p3ocupados": { + "type": "integer" + }, + "p4ocupados": { + "type": "integer" + }, + "p1livres": { + "type": "integer" + }, + "p3livres": { + "type": "integer" + }, + "p4livres": { + "type": "integer" + } + }, + "required": [ + "p1lotacao", + "p3lotacao", + "p4lotacao", + "data", + "P1in", + "P1out", + "P3in", + "P3out", + "P4in", + "P4out", + "p1ocupados", + "p3ocupados", + "p4ocupados", + "p1livres", + "p3livres", + "p4livres" + ] + } + }, + "required": [ + "versao", + "dataresposta", + "codigoresposta", + "statementsql", + "linhas", + "erro", + "resposta" + ] + } + } + }, + "required": [ + "itdc" + ] + } + } + } + } + }, + "security": [] + } + }, + "/calendarios-api/api/v1/events/{faculty}/student/{nmec}/": { + "get": { + "operationId": "calendar-get-student-events", + "tags": [ + "calendar" + ], + "summary": "Student schedule (v2)", + "description": "The session works from any IP address, not only the one that created it.\n\nA student's schedule, from SIGARRA's new schedule module.\n\nThe trailing slash is required: without it SIGARRA redirects to `/calendarios/...`, which returns 404. The URL for a student is in the `data-evt-source-url` and `data-table-source-url` attributes of `hor_geral.estudantes_view`.", + "x-fern-sdk-group-name": "calendar", + "x-fern-sdk-method-name": "getStudentEvents", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "name": "nmec", + "in": "path", + "required": true, + "description": "Student number.", + "schema": { + "type": "string" + } + }, + { + "name": "academic_year", + "in": "query", + "required": true, + "description": "First year of the academic year, e.g. `2023` for 2023/2024.", + "schema": { + "type": "integer" + } + }, + { + "name": "pv_turma_ocorr", + "in": "query", + "required": true, + "style": "form", + "explode": true, + "description": "Classes to include, as `{FACULTY}_{ocorr_id}_{turma_id}` (e.g. `FEUP_518805_213039`). At least one is required.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "period", + "in": "query", + "style": "form", + "explode": true, + "description": "Teaching periods to include.", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEvents" + } + } + } + }, + "400": { + "description": "Missing `academic_year` or `pv_turma_ocorr`." + }, + "403": { + "description": "No valid session. The body is empty." + } + }, + "deprecated": false + } + }, + "/calendarios-api/api/v1/caltable/{faculty}/student/{nmec}/": { + "get": { + "operationId": "calendar-get-student-table", + "tags": [ + "calendar" + ], + "summary": "Student schedule table (v2)", + "description": "The session works from any IP address, not only the one that created it.\n\nA student's schedule, from SIGARRA's new schedule module, as an HTML table.\n\nThe trailing slash is required: without it SIGARRA redirects to `/calendarios/...`, which returns 404. The URL for a student is in the `data-evt-source-url` and `data-table-source-url` attributes of `hor_geral.estudantes_view`.", + "x-fern-sdk-group-name": "calendar", + "x-fern-sdk-method-name": "getStudentTable", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "name": "nmec", + "in": "path", + "required": true, + "description": "Student number.", + "schema": { + "type": "string" + } + }, + { + "name": "academic_year", + "in": "query", + "required": true, + "description": "First year of the academic year, e.g. `2023` for 2023/2024.", + "schema": { + "type": "integer" + } + }, + { + "name": "pv_turma_ocorr", + "in": "query", + "required": true, + "style": "form", + "explode": true, + "description": "Classes to include, as `{FACULTY}_{ocorr_id}_{turma_id}` (e.g. `FEUP_518805_213039`). At least one is required.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "period", + "in": "query", + "style": "form", + "explode": true, + "description": "Teaching periods to include.", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "An HTML `` fragment.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Missing `academic_year` or `pv_turma_ocorr`." + }, + "403": { + "description": "No valid session. The body is empty." + } + } + } + }, + "/calendarios-api/api/v1/events/{faculty}/uc/{ocorr_id}/": { + "get": { + "operationId": "calendar-get-course-unit-events", + "tags": [ + "calendar" + ], + "summary": "Course unit schedule (v2)", + "description": "The session works from any IP address, not only the one that created it.\n\nThe trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "calendar", + "x-fern-sdk-method-name": "getCourseUnitEvents", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "name": "ocorr_id", + "in": "path", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + }, + { + "name": "academic_year", + "in": "query", + "required": true, + "description": "First year of the academic year, e.g. `2023` for 2023/2024.", + "schema": { + "type": "integer" + } + }, + { + "name": "period", + "in": "query", + "required": true, + "style": "form", + "explode": true, + "description": "Teaching periods to include. `uni` sends 1–8.", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEvents" + } + } + } + }, + "400": { + "description": "Missing `academic_year` or `period`." + }, + "403": { + "description": "No valid session. The body is empty." + } + } + } + }, + "/calendarios-api/api/v1/caltable/{faculty}/uc/{ocorr_id}/": { + "get": { + "operationId": "calendar-get-course-unit-table", + "tags": [ + "calendar" + ], + "summary": "Course unit schedule table (v2)", + "description": "The session works from any IP address, not only the one that created it.\n\nThe trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "calendar", + "x-fern-sdk-method-name": "getCourseUnitTable", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "name": "ocorr_id", + "in": "path", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + }, + { + "name": "academic_year", + "in": "query", + "required": true, + "description": "First year of the academic year, e.g. `2023` for 2023/2024.", + "schema": { + "type": "integer" + } + }, + { + "name": "period", + "in": "query", + "required": true, + "style": "form", + "explode": true, + "description": "Teaching periods to include. `uni` sends 1–8.", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "An HTML `
` fragment.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Missing `academic_year` or `period`." + }, + "403": { + "description": "No valid session. The body is empty." + } + } + } + }, + "/calendarios-api/api/v1/events/{faculty}/doc/{doc_codigo}/": { + "get": { + "operationId": "calendar-get-professor-events", + "tags": [ + "calendar" + ], + "summary": "Professor schedule (v2)", + "description": "The session works from any IP address, not only the one that created it.\n\nThe trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "calendar", + "x-fern-sdk-method-name": "getProfessorEvents", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "name": "doc_codigo", + "in": "path", + "required": true, + "description": "Professor code.", + "schema": { + "type": "string" + } + }, + { + "name": "academic_year", + "in": "query", + "required": true, + "description": "First year of the academic year, e.g. `2023` for 2023/2024.", + "schema": { + "type": "integer" + } + }, + { + "name": "period", + "in": "query", + "required": true, + "style": "form", + "explode": true, + "description": "Teaching periods to include. `uni` sends 1–8.", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CalendarEvents" + } + } + } + }, + "400": { + "description": "Missing `academic_year` or `period`." + }, + "403": { + "description": "No valid session. The body is empty." + } + } + } + }, + "/calendarios-api/api/v1/caltable/{faculty}/doc/{doc_codigo}/": { + "get": { + "operationId": "calendar-get-professor-table", + "tags": [ + "calendar" + ], + "summary": "Professor schedule table (v2)", + "description": "The session works from any IP address, not only the one that created it.\n\nThe trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "calendar", + "x-fern-sdk-method-name": "getProfessorTable", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "name": "doc_codigo", + "in": "path", + "required": true, + "description": "Professor code.", + "schema": { + "type": "string" + } + }, + { + "name": "academic_year", + "in": "query", + "required": true, + "description": "First year of the academic year, e.g. `2023` for 2023/2024.", + "schema": { + "type": "integer" + } + }, + { + "name": "period", + "in": "query", + "required": true, + "style": "form", + "explode": true, + "description": "Teaching periods to include. `uni` sends 1–8.", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "An HTML `
` fragment.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Missing `academic_year` or `period`." + }, + "403": { + "description": "No valid session. The body is empty." + } + } + } + }, + "/{faculty}/{language}/mob_val_geral.autentica": { + "post": { + "operationId": "auth-login", + "tags": [ + "auth" + ], + "summary": "Login (credentials, JSON)", + "description": "Authenticates with a username and password and answers with JSON; on success, sets the session cookies.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "auth", + "x-fern-sdk-method-name": "login", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "pv_login": { + "type": "string", + "description": "Username, e.g. `up20YYXXXXX`." + }, + "pv_password": { + "type": "string", + "format": "password" + } + }, + "required": [ + "pv_login", + "pv_password" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Authenticated.", + "headers": { + "Set-Cookie": { + "description": "On success, `SI_SESSION` (`HttpOnly`) and `SI_SECURITY` (`Secure; HttpOnly`), without an expiry date.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResult" + } + } + } + }, + "403": { + "description": "Wrong username or password.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "security": [] + }, + "get": { + "operationId": "auth-login-with-query", + "tags": [ + "auth" + ], + "summary": "Login (credentials, JSON, query string)", + "description": "Same as `POST`, with the credentials in the query string. Prefer `POST`: URLs (and so the password) tend to end up in logs.\n\nUsed by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper).", + "x-fern-sdk-group-name": "auth", + "x-fern-sdk-method-name": "loginWithQuery", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_login", + "in": "query", + "required": true, + "description": "Username, e.g. `up20YYXXXXX`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_password", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "password" + } + } + ], + "responses": { + "200": { + "description": "Authenticated.", + "headers": { + "Set-Cookie": { + "description": "On success, `SI_SESSION` (`HttpOnly`) and `SI_SECURITY` (`Secure; HttpOnly`), without an expiry date.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResult" + } + } + } + }, + "403": { + "description": "Wrong username or password.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/{faculty}/{language}/vld_validacao.validacao": { + "post": { + "operationId": "auth-login-page", + "tags": [ + "auth" + ], + "summary": "Login (credentials)", + "description": "Authenticates with a username and password; this is the website's login form (served by `vld_validacao.login`). Works on any faculty path, including `up`.\n\nOn success, sets `SI_SESSION` and `SI_SECURITY` (no expiry date; the session lasts about 2 hours) and redirects with a `` to `p_address` (`web_page.Inicial` by default). On failure, the reason is in `p.aviso-invalidado` (e.g. \"O conjunto utilizador/senha não é válido.\" for a wrong username or password, \"A sua conta encontra-se bloqueada.\" for a locked account, or an expired password). If the request already carries a valid session, the page is titled \"Problema encontrado\" (\"Problem found\"), `p.erro-nota` says \"Já se encontra autenticado com o utilizador .\" (\"You are already authenticated as .\"), and no new session is created.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "auth", + "x-fern-sdk-method-name": "loginPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "p_user": { + "type": "string", + "description": "Username, with or without the `up` prefix (e.g. `up20YYXXXXX` or `20YYXXXXX`)." + }, + "p_pass": { + "type": "string", + "format": "password" + }, + "p_address": { + "type": "string", + "description": "Page to redirect to after logging in. Defaults to `web_page.Inicial`." + }, + "p_app": { + "type": "string" + }, + "p_amo": { + "type": "string" + } + }, + "required": [ + "p_user", + "p_pass" + ] + } + } + } + }, + "responses": { + "200": { + "description": "HTML page. See the description for how to tell success from failure.", + "headers": { + "Set-Cookie": { + "description": "On success, `SI_SESSION` (`HttpOnly`) and `SI_SECURITY` (`Secure; HttpOnly`), without an expiry date.", + "schema": { + "type": "string" + } + } + }, + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [] + } + }, + "/{faculty}/{language}/vld_validacao.sair": { + "get": { + "operationId": "auth-logout", + "tags": [ + "auth" + ], + "summary": "Logout", + "description": "Ends the session: its cookies stop working. Then redirects to `web_page.Inicial` with a ``. Answers the same without a session.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "auth", + "x-fern-sdk-method-name": "logout", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "responses": { + "200": { + "description": "HTML page that redirects to the home page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + {}, + { + "siSession": [], + "siSecurity": [] + } + ] + } + }, + "/auth/oidc/token": { + "get": { + "operationId": "auth-oidc-token", + "tags": [ + "auth" + ], + "summary": "Login (OpenID Connect)", + "description": "Exchanges a token from the U.Porto identity provider (`https://open-id.up.pt/realms/sigarra`) for SIGARRA session cookies. Both access tokens and ID tokens are accepted. The session is bound to the IP address that makes this call, so call it from wherever the session will be used (e.g. a backend, not the browser that obtained the token). Each call creates a new session that lasts 2 hours (`exp` is 7200 s after the request), regardless of the token's own lifetime. [uni](https://github.com/NIAEFEUP/uni)'s tokens have the audience `it-sigarra-oidc-bridge`, likely the name of this service.\n\nAnswers `401` without a token and `403` with an invalid one. Only `GET` is allowed (`405` otherwise).\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "auth", + "x-fern-sdk-method-name": "oidcToken", + "parameters": [], + "responses": { + "200": { + "description": "Authenticated.", + "headers": { + "Set-Cookie": { + "description": "`SI_SESSION` and `SI_SECURITY`, with `Domain=sigarra.up.pt` and an `Expires` matching `exp`.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "exp", + "result" + ], + "properties": { + "exp": { + "type": "integer", + "description": "When the session cookies expire, as a Unix timestamp: 2 hours after the request." + }, + "result": { + "type": "string", + "description": "`OK` on success." + } + } + } + } + } + }, + "401": { + "description": "Missing token." + }, + "403": { + "description": "Invalid token.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "uportoOidc": [] + } + ] + } + }, + "/{faculty}/{language}/web_page.inicial": { + "get": { + "operationId": "home-get-home-page", + "tags": [ + "home" + ], + "summary": "Home page", + "description": "Home page. When authenticated, `.autenticado .fotografia img` links to `fotografias_service.foto?pct_cod=`, which `uni` uses to find the username.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "home", + "x-fern-sdk-method-name": "getHomePage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + {}, + { + "siSession": [], + "siSecurity": [] + } + ] + } + }, + "/{faculty}/{language}/vld_entidades_geral.entidade_pagina": { + "get": { + "operationId": "student-get-faculties-page", + "tags": [ + "student" + ], + "summary": "A person's faculties", + "description": "Without a valid session, SIGARRA answers `200` with a redirect page to another faculty instead of the list.\n\nLists the faculties a person belongs to (`#conteudoinner>ul a`, e.g. \"Faculdade de Engenharia (FEUP)\"). `uni` requests it with `faculty` set to `up`.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getFacultiesPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pct_codigo", + "in": "query", + "required": true, + "description": "Username.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/fest_geral.cursos_list": { + "get": { + "operationId": "student-get-courses-page", + "tags": [ + "student" + ], + "summary": "A student's courses", + "description": "Lists a student's courses and links to each `fest_geral.curso_percurso_academico_view?pv_fest_id=`. `uni` also uses it to check whether the session is still valid.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getCoursesPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_num_unico", + "in": "query", + "required": true, + "description": "Student number.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/fest_geral.curso_percurso_academico_view": { + "get": { + "operationId": "student-get-course-academic-path-page", + "tags": [ + "student" + ], + "summary": "Academic path in a course", + "description": "A student's academic path in a course: course units, grades and the course average.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getCourseAcademicPathPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_fest_id", + "in": "query", + "required": true, + "description": "Course enrolment ID (`fest_id`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/fest_geral.ucurr_inscricoes_list": { + "get": { + "operationId": "student-get-course-unit-enrollments-page", + "tags": [ + "student" + ], + "summary": "Course unit enrolments", + "description": "A student's course unit enrolments in a course.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getCourseUnitEnrollmentsPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_fest_id", + "in": "query", + "required": true, + "description": "Course enrolment ID (`fest_id`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/fest_geral.info_pessoal_view": { + "get": { + "operationId": "student-get-personal-info-page", + "tags": [ + "student" + ], + "summary": "Personal information", + "description": "Without a valid session, SIGARRA answers `200` with a one-line error (\"Erro - Não tem permissões de acesso a esta página.\", i.e. no permission to access the page).\n\nA student's personal details.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getPersonalInfoPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_num_unico", + "in": "query", + "required": true, + "description": "Student number.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_dummy", + "in": "query", + "required": true, + "description": "Required, but any value works (even empty). `uni` sends the current timestamp.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/hor_geral.estudantes_view": { + "get": { + "operationId": "student-get-schedule-page", + "tags": [ + "student" + ], + "summary": "Student schedule page", + "description": "A student's schedule page. `#cal-shadow-container[data-evt-source-url]` has the URL for `calendar.getStudentEvents`.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "student", + "x-fern-sdk-method-name": "getSchedulePage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_num_unico", + "in": "query", + "required": true, + "description": "Student number.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo", + "in": "query", + "required": true, + "description": "Academic year, e.g. `2023`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_periodos", + "in": "query", + "required": true, + "description": "`uni` sends `1`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/exa_geral.mapa_de_exames": { + "get": { + "operationId": "course-unit-get-exams-page", + "tags": [ + "courseUnit" + ], + "summary": "Exam calendar", + "description": "Exam calendar of a course unit occurrence; exams are in `td.exame` cells.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getExamsPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "p_ocorr_id", + "in": "query", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/hor_geral.ucurr_view": { + "get": { + "operationId": "course-unit-get-schedule-page", + "tags": [ + "courseUnit" + ], + "summary": "Course unit schedule page", + "description": "A course unit's schedule page. `#cal-shadow-container[data-evt-source-url]` has the URL for `calendar.getCourseUnitEvents`.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getSchedulePage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo", + "in": "query", + "required": true, + "description": "Academic year, e.g. `2023`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_periodos", + "in": "query", + "required": true, + "description": "`uni` sends `1`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/it_listagem.lista_cursos_disciplina": { + "get": { + "operationId": "course-unit-get-courses-page", + "tags": [ + "courseUnit" + ], + "summary": "Courses of a course unit", + "description": "Lists the courses a course unit occurrence belongs to, with links to `it_listagem.lista_turma_disciplina` for each.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getCoursesPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [] + } + }, + "/{faculty}/{language}/it_listagem.lista_turma_disciplina": { + "get": { + "operationId": "course-unit-get-classes-page", + "tags": [ + "courseUnit" + ], + "summary": "Classes and students of a course unit", + "description": "Lists a course unit's classes and their students, for one course. `uni` follows the links in `it_listagem.lista_cursos_disciplina`.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getClassesPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_curso_id", + "in": "query", + "required": true, + "description": "Course ID.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo", + "in": "query", + "required": true, + "description": "Academic year, e.g. `2023`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_periodo_id", + "in": "query", + "required": true, + "description": "Period ID.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_no_menu", + "in": "query", + "required": false, + "description": "Set to `1` to omit the page menu.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/func_geral.formview": { + "get": { + "operationId": "professor-get-profile-page", + "tags": [ + "professor" + ], + "summary": "Staff member page", + "description": "Public, but shows more information with a valid session.\n\nA staff member's page. `uni` extracts the institutional email (from `mailto:` links or an obfuscated `onclick`) and offices (`instal_geral.espaco_view` links).\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "professor", + "x-fern-sdk-method-name": "getProfilePage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "p_codigo", + "in": "query", + "required": true, + "description": "Staff code.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + {}, + { + "siSession": [], + "siSecurity": [] + } + ] + } + }, + "/{faculty}/{language}/hor_geral.docentes_view": { + "get": { + "operationId": "professor-get-schedule-page", + "tags": [ + "professor" + ], + "summary": "Professor schedule page", + "description": "A professor's schedule page. `#cal-shadow-container[data-evt-source-url]` has the URL for `calendar.getProfessorEvents`.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "professor", + "x-fern-sdk-method-name": "getSchedulePage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_doc_codigo", + "in": "query", + "required": true, + "description": "Professor code.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo", + "in": "query", + "required": true, + "description": "Academic year, e.g. `2023`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_periodos", + "in": "query", + "required": true, + "description": "`uni` sends `1`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/fotografias_service.foto": { + "get": { + "operationId": "people-get-photo", + "tags": [ + "people" + ], + "summary": "Photo", + "description": "Without a valid session, SIGARRA answers `200` with a placeholder image.\n\nPhoto of a student or staff member.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "people", + "x-fern-sdk-method-name": "getPhoto", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pct_cod", + "in": "query", + "required": true, + "description": "Student number or staff code.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The photo (e.g. `image/png`).", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "security": [ + {}, + { + "siSession": [], + "siSecurity": [] + } + ] + } + }, + "/{faculty}/{language}/gpag_ccorrente_geral.conta_corrente_view": { + "get": { + "operationId": "finances-get-current-account-page", + "tags": [ + "finances" + ], + "summary": "Current account", + "description": "A person's current account: unpaid fees, payment references and the account statement. `uni` also requests it on the `sasup` faculty for social services.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "finances", + "x-fern-sdk-method-name": "getCurrentAccountPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pct_cod", + "in": "query", + "required": true, + "description": "Username.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/imp4_impressoes.atribs": { + "get": { + "operationId": "finances-get-printing-page", + "tags": [ + "finances" + ], + "summary": "Printing balance", + "description": "Printing balance and history. `uni` requests it on `feup`.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "finances", + "x-fern-sdk-method-name": "getPrintingPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "p_codigo", + "in": "query", + "required": true, + "description": "Username.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/gpag_ccorrentes_geral.gerar_mb": { + "post": { + "operationId": "finances-generate-mb-reference", + "tags": [ + "finances" + ], + "summary": "Generate a Multibanco reference", + "description": "Creates a Multibanco payment reference, e.g. to top up the printing balance. **This creates a real payment reference.** Not verified live.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "finances", + "x-fern-sdk-method-name": "generateMbReference", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "p_tipo_id": { + "type": "string", + "description": "Reference type. `uni` sends `3` (printing)." + }, + "pct_codigo": { + "type": "string", + "description": "Username." + }, + "p_valor": { + "type": "string", + "description": "`uni` sends `1`." + }, + "p_valor_livre": { + "type": "string", + "description": "Amount, with a decimal comma (e.g. `5,00`). Minimum 1,00 €." + } + }, + "required": [ + "p_tipo_id", + "pct_codigo", + "p_valor", + "p_valor_livre" + ] + } + } + } + }, + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/CANTINA.EMENTASHOW": { + "get": { + "operationId": "canteen-get-menus-page", + "tags": [ + "canteen" + ], + "summary": "Canteen menus", + "description": "Canteen menus. `uni` requests it on `feup`.\n\nUsed by [uni](https://github.com/NIAEFEUP/uni).", + "x-fern-sdk-group-name": "canteen", + "x-fern-sdk-method-name": "getMenusPage", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [] + } + }, + "/{faculty}/{language}/vld_validacao.login": { + "get": { + "operationId": "auth-get-login-page", + "tags": [ + "auth" + ], + "summary": "Login form", + "description": "The login page: a credentials form that posts to `vld_validacao.validacao`, and a federated login form that submits to `vld_validacao.federate_login`.", + "x-fern-sdk-group-name": "auth", + "x-fern-sdk-method-name": "getLoginPage", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "p_address", + "in": "query", + "description": "Page to redirect to after logging in, e.g. `WEB_PAGE.INICIAL`.", + "schema": { + "type": "string" + } + }, + { + "name": "p_app", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "p_amo", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/vld_validacao.federate_login": { + "get": { + "operationId": "auth-federated-login", + "tags": [ + "auth" + ], + "summary": "Login (SAML)", + "description": "Starts the browser login through U.Porto's Shibboleth identity provider: redirects to `https://wayf.up.pt/idp/profile/SAML2/Redirect/SSO` with a SAML `AuthnRequest`. SIGARRA receives the response at `/Shibboleth.sso/SAML2/POST`.", + "x-fern-sdk-group-name": "auth", + "x-fern-sdk-method-name": "federatedLogin", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "p_redirect", + "in": "query", + "required": true, + "description": "Page to return to after logging in, e.g. `WEB_PAGE.INICIAL`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Redirect to the identity provider.", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/mob_hor_geral.ucurr": { + "get": { + "operationId": "course-unit-get-schedule", + "tags": [ + "courseUnit" + ], + "summary": "Course unit schedule", + "description": "Weekly timetable of a course unit occurrence. Returns the weekly timetable of the week that contains `pv_semana_ini`; `pv_semana_fim` does not widen the range.", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getSchedule", + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_semana_ini", + "in": "query", + "required": true, + "description": "Start of the period, as YYYYMMDD.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_semana_fim", + "in": "query", + "required": true, + "description": "End of the period, as YYYYMMDD.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WeeklySchedule" + } + } + } + }, + "403": { + "description": "No valid session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/{faculty}/{language}/web_base.gera_pagina": { + "get": { + "operationId": "home-get-faculties-redirect", + "tags": [ + "home" + ], + "summary": "List of faculties (redirect)", + "description": "Institutional page generator. Only `/up/pt/web_base.gera_pagina?p_pagina=escolas` (the list of faculties) still answers: it redirects (`301`) to `https://www.up.pt/portal/pt/conhecer/sobre-a-uporto/as-faculdades/`. Any other combination (another faculty, another page, or no `p_pagina`) is refused by the web server with a generic `403`.\n\nUsed by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper).", + "x-fern-sdk-group-name": "home", + "x-fern-sdk-method-name": "getFacultiesRedirect", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "p_pagina", + "in": "query", + "required": true, + "description": "Page name. Only `escolas` works (with `faculty` = `up`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "301": { + "description": "Redirect to the list of faculties on `www.up.pt`.", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Refused by the web server (generic Apache page).", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/cur_geral.cur_view": { + "get": { + "operationId": "course-get-page", + "tags": [ + "course" + ], + "summary": "Course page", + "description": "A course page: name, acronym (the \"Sigla:\" / \"Acronym:\" row), and a \"Planos de Estudos\" (study plans) section linking to `cur_geral.cur_planos_estudos_view`. An unknown course answers `200` with a \"Sem resultados\" (no results) page.\n\nUsed by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper).", + "x-fern-sdk-group-name": "course", + "x-fern-sdk-method-name": "getPage", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_curso_id", + "in": "query", + "required": true, + "description": "Course ID.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo", + "in": "query", + "required": false, + "description": "Academic year, e.g. `2023`. Defaults to the current one.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_origem", + "in": "query", + "required": false, + "description": "Where the link came from, e.g. `CUR`. Doesn't change the content.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_tipo_cur_sigla", + "in": "query", + "required": false, + "description": "Course type: `A` refresher course, `B` pre-Bologna bachelor's degree (*bacharelato*), `CF` complementary training course, `CFC` continuing education course, `VI` summer/winter course, `FL` free course or training, `D` doctorate, `E` specialisation, `EA` advanced studies, `EG` EUGLOH, `EIA` European Innovation Academy, `FC` continuing education, `L` bachelor's degree (*licenciatura*), `LV` free, `M` master's degree, `MI` integrated master's degree, `PD` postdoctoral programme, `UFC` continuing education unit, `UM` unit/module/training action.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/cur_geral.cur_planos_estudos_view": { + "get": { + "operationId": "course-get-study-plan-page", + "tags": [ + "course" + ], + "summary": "Study plan", + "description": "A course's study plan. Each group of course units is a `div[id^=\"div_id_\"]` with an `h3` title and a `table.dadossz` whose rows link to `ucurr_geral.ficha_uc_view?pv_ocorrencia_id=`.\n\nUsed by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper).", + "x-fern-sdk-group-name": "course", + "x-fern-sdk-method-name": "getStudyPlanPage", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_plano_id", + "in": "query", + "required": true, + "description": "Study plan ID, from the course page.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_ano_lectivo", + "in": "query", + "required": true, + "description": "Academic year, e.g. `2023`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_tipo_cur_sigla", + "in": "query", + "required": false, + "description": "Course type: `A` refresher course, `B` pre-Bologna bachelor's degree (*bacharelato*), `CF` complementary training course, `CFC` continuing education course, `VI` summer/winter course, `FL` free course or training, `D` doctorate, `E` specialisation, `EA` advanced studies, `EG` EUGLOH, `EIA` European Innovation Academy, `FC` continuing education, `L` bachelor's degree (*licenciatura*), `LV` free, `M` master's degree, `MI` integrated master's degree, `PD` postdoctoral programme, `UFC` continuing education unit, `UM` unit/module/training action.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_origem", + "in": "query", + "required": false, + "description": "Where the link came from, e.g. `CUR`. Doesn't change the content.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/it_geral.vagas": { + "get": { + "operationId": "course-get-class-vacancies-page", + "tags": [ + "course" + ], + "summary": "Class vacancies", + "description": "Vacancies in each class of a course's course units, for the current class enrolment period (e.g. \"Inscrições nas turmas 1º Semestre - 2026/2027\", the 1st semester's class enrolments). The data is a table with one row per course unit and pairs of columns (class, vacancies). The course doesn't need to belong to the faculty in the path.\n\nUsed by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper).", + "x-fern-sdk-group-name": "course", + "x-fern-sdk-method-name": "getClassVacanciesPage", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_curso_id", + "in": "query", + "required": false, + "description": "Course ID. Without it, the page has no table.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_colunas", + "in": "query", + "required": false, + "description": "How many class columns per row, `3` to `9` (default `8`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/ucurr_geral.pesquisa_ocorr_ucs_list": { + "get": { + "operationId": "course-unit-search-page", + "tags": [ + "courseUnit" + ], + "summary": "Course unit search", + "description": "Search results for course unit occurrences (the form is `ucurr_geral.pesquisa_ucs`). Each result row (`table.dados .d`) links to `ucurr_geral.ficha_uc_view?pv_ocorrencia_id=`; results are paginated, and the last page is linked from `.paginar-saltar-barra-posicao`. All filters are optional.\n\nUsed by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper).", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "searchPage", + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_ano_lectivo", + "in": "query", + "required": false, + "description": "Academic year, e.g. `2023`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_curso_id", + "in": "query", + "required": false, + "description": "Course ID.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_curso_nome", + "in": "query", + "required": false, + "description": "Course name.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_uc_codigo", + "in": "query", + "required": false, + "description": "Course unit code, e.g. `M.EIC001`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_uc_sigla", + "in": "query", + "required": false, + "description": "Course unit acronym, e.g. `AC`.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_uc_nome", + "in": "query", + "required": false, + "description": "Course unit name.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_uc_ano", + "in": "query", + "required": false, + "description": "Curricular year.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_uc_periodo", + "in": "query", + "required": false, + "description": "Period: `1` annual, `2` 1st semester, `3` 2nd semester, `4`–`7` 1st–4th trimester, `8` no period.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_doc_codigo", + "in": "query", + "required": false, + "description": "Teacher code.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_doc_sigla", + "in": "query", + "required": false, + "description": "Teacher acronym.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_doc_nome", + "in": "query", + "required": false, + "description": "Teacher name.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_lingua", + "in": "query", + "required": false, + "description": "Teaching language.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_tipo_curso", + "in": "query", + "required": false, + "description": "Course type: `A` refresher course, `B` pre-Bologna bachelor's degree (*bacharelato*), `CF` complementary training course, `CFC` continuing education course, `VI` summer/winter course, `FL` free course or training, `D` doctorate, `E` specialisation, `EA` advanced studies, `EG` EUGLOH, `EIA` European Innovation Academy, `FC` continuing education, `L` bachelor's degree (*licenciatura*), `LV` free, `M` master's degree, `MI` integrated master's degree, `PD` postdoctoral programme, `UFC` continuing education unit, `UM` unit/module/training action.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_tipo_avaliacao", + "in": "query", + "required": false, + "description": "Assessment type.", + "schema": { + "type": "string" + } + }, + { + "name": "pv_num_pag", + "in": "query", + "required": false, + "description": "Page number, from `1`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{faculty}/{language}/ucurr_geral.ficha_uc_view": { + "get": { + "operationId": "course-unit-get-sheet-page", + "tags": [ + "courseUnit" + ], + "summary": "Course unit page", + "description": "A course unit occurrence's page: name, acronym (\"Sigla:\" / \"Acronym:\"), occurrence (`h2`, e.g. \"Ocorrência: 2023/2024 - 1S\"), and the \"Ciclos de Estudo/Cursos\" (study cycles/courses) table (course, curricular year, ECTS). With a valid session it also shows teaching responsibilities and hours, and navigation links (contents, students, schedule, classes, exams, …); in particular, the schedule link, `Horário`, links to `hor_geral.ucurr_view?pv_ocorrencia_id=` only with a session. An unknown occurrence answers `200` with a \"Sem Resultados\" (no results) page.\n\nUsed by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper).", + "x-fern-sdk-group-name": "courseUnit", + "x-fern-sdk-method-name": "getSheetPage", + "security": [ + {}, + { + "siSession": [], + "siSecurity": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/Faculty" + }, + { + "$ref": "#/components/parameters/Language" + }, + { + "name": "pv_ocorrencia_id", + "in": "query", + "required": true, + "description": "Course unit occurrence ID.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page.", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CalendarEvents": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "state": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "code", + "name" + ] + }, + "is_inst_req": { + "type": "boolean" + }, + "hour_start": { + "type": "string" + }, + "hour_end": { + "type": "string" + }, + "first_date": { + "type": "string" + }, + "last_date": { + "type": "string" + }, + "week_days": { + "type": "array", + "items": { + "type": "string" + } + }, + "week_count": { + "type": "integer" + }, + "ucs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "acronym": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sigarra_id": { + "type": "integer" + }, + "sigarra_url": { + "type": "string" + } + }, + "required": [ + "id", + "acronym", + "name", + "sigarra_id", + "sigarra_url" + ] + } + }, + "classes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "acronym": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sigarra_id": { + "type": "integer" + }, + "sigarra_url": { + "type": "string" + } + }, + "required": [ + "id", + "acronym", + "name", + "sigarra_id", + "sigarra_url" + ] + } + }, + "persons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "acronym": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sigarra_id": { + "type": [ + "integer", + "null" + ] + }, + "sigarra_url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "acronym", + "name", + "sigarra_id", + "sigarra_url" + ] + } + }, + "rooms": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "acronym": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sigarra_id": { + "type": "integer" + }, + "sigarra_url": { + "type": "string" + } + }, + "required": [ + "id", + "acronym", + "name", + "sigarra_id", + "sigarra_url" + ] + } + }, + "typology": { + "type": "object", + "properties": { + "acronym": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "acronym", + "name" + ] + }, + "css_class": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "start", + "end", + "id", + "name", + "state", + "is_inst_req", + "hour_start", + "hour_end", + "first_date", + "last_date", + "week_days", + "week_count", + "ucs", + "classes", + "persons", + "rooms", + "typology", + "css_class" + ] + } + }, + "meta": { + "type": "object", + "properties": { + "legend": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "acronym": { + "type": "string" + }, + "color_class": { + "type": "string" + } + }, + "required": [ + "name", + "acronym", + "color_class" + ] + } + }, + "cal_type": { + "type": "string" + }, + "cal_params": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "academic_year_range": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + } + }, + "required": [ + "start", + "end" + ] + }, + "day_time_range": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + } + }, + "required": [ + "start", + "end" + ] + }, + "lunch_time_range": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + } + }, + "required": [ + "start", + "end" + ] + } + }, + "required": [ + "locale", + "academic_year_range", + "day_time_range", + "lunch_time_range" + ] + }, + "date_range": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + } + }, + "required": [ + "start", + "end" + ] + }, + "count": { + "type": "integer" + } + }, + "required": [ + "legend", + "cal_type", + "cal_params", + "date_range", + "count" + ] + } + }, + "required": [ + "data", + "meta" + ] + }, + "Error": { + "type": "object", + "required": [ + "erro", + "erro_msg" + ], + "properties": { + "erro": { + "type": "string", + "description": "Error type, e.g. `Autorização`." + }, + "erro_msg": { + "type": "string", + "description": "Error message." + } + } + }, + "WeeklySchedule": { + "type": "object", + "properties": { + "horario": { + "type": "array", + "items": { + "type": "object", + "properties": { + "dia": { + "type": "integer" + }, + "hora_inicio": { + "type": "integer" + }, + "ucurr_sigla": { + "type": "string" + }, + "ocorrencia_id": { + "type": "integer" + }, + "tipo": { + "type": "string" + }, + "aula_id": { + "type": "integer" + }, + "aula_duracao": { + "type": "integer" + }, + "sala_sigla": { + "type": "string" + }, + "salas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "espaco_id": { + "type": "integer" + }, + "espaco_nome": { + "type": "string" + } + }, + "required": [ + "espaco_id", + "espaco_nome" + ] + } + }, + "doc_sigla": { + "type": "string" + }, + "docentes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "doc_codigo": { + "type": "string" + }, + "doc_nome": { + "type": "string" + } + }, + "required": [ + "doc_codigo", + "doc_nome" + ] + } + }, + "turma_sigla": { + "type": "string" + }, + "turmas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "turma_id": { + "type": "integer" + }, + "turma_sigla": { + "type": "string" + } + }, + "required": [ + "turma_id", + "turma_sigla" + ] + } + }, + "periodo": { + "type": "integer" + } + }, + "required": [ + "dia", + "hora_inicio", + "ucurr_sigla", + "ocorrencia_id", + "tipo", + "aula_id", + "aula_duracao", + "sala_sigla", + "salas", + "doc_sigla", + "docentes", + "turma_sigla", + "turmas", + "periodo" + ] + } + }, + "blocos": { + "type": "array", + "items": { + "type": "object", + "properties": { + "semana_ini": { + "type": "string" + }, + "semana_fim": { + "type": "string" + }, + "bloco_codigo": { + "type": "null" + }, + "c_turma_id": { + "type": "null" + }, + "turmas_bloco": { + "type": "string" + }, + "turma_real": { + "type": "string" + }, + "periodo": { + "type": "string" + } + }, + "required": [ + "semana_ini", + "semana_fim", + "bloco_codigo", + "c_turma_id", + "turmas_bloco", + "turma_real", + "periodo" + ] + } + } + }, + "required": [ + "horario" + ] + }, + "LoginResult": { + "type": "object", + "required": [ + "authenticated", + "codigo", + "tipo" + ], + "properties": { + "authenticated": { + "type": "boolean" + }, + "codigo": { + "type": "string", + "description": "Username, without the `up` prefix (e.g. `20YYXXXXX`)." + }, + "tipo": { + "type": "string", + "description": "User type: `A` for students. Values for other kinds of users haven't been observed." + } + } + } + }, + "parameters": { + "Faculty": { + "name": "faculty", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "feup" + }, + "Language": { + "name": "language", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "pt" + } + }, + "securitySchemes": { + "siSession": { + "type": "apiKey", + "in": "cookie", + "name": "SI_SESSION", + "description": "Session cookie. Must be sent together with `SI_SECURITY`. Only valid from the IP address that created the session (except for the calendar API)." + }, + "siSecurity": { + "type": "apiKey", + "in": "cookie", + "name": "SI_SECURITY", + "description": "Session cookie. Must be sent together with `SI_SESSION`. Only valid from the IP address that created the session (except for the calendar API)." + }, + "uportoOidc": { + "type": "openIdConnect", + "openIdConnectUrl": "https://open-id.up.pt/realms/sigarra/.well-known/openid-configuration", + "description": "U.Porto identity provider. The access token (or ID token) is sent as `Authorization: Bearer `." + } + } + }, + "security": [ + { + "siSession": [], + "siSecurity": [] + } + ] +} diff --git a/packages/sigarra/package.json b/packages/sigarra/package.json new file mode 100644 index 000000000..ef1013b90 --- /dev/null +++ b/packages/sigarra/package.json @@ -0,0 +1,17 @@ +{ + "name": "@tts/sigarra", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "generate": "fern generate --local --force && bun scripts/patch-generated.ts", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "^25.6.2", + "fern-api": "5.144.3", + "typescript": "^6.0.3" + } +} diff --git a/packages/sigarra/scripts/patch-generated.ts b/packages/sigarra/scripts/patch-generated.ts new file mode 100644 index 000000000..535b83112 --- /dev/null +++ b/packages/sigarra/scripts/patch-generated.ts @@ -0,0 +1,63 @@ +// Patches the Fern-generated SDK after `fern generate`. +// +// Headers.ts: the fallback Headers class declares its iterators as +// IterableIterator, but current DOM/Node typings require HeadersIterator (which +// adds the disposable iterator members), so the generated file fails to +// typecheck. Runtime behaviour is identical; only the declared return types change. +// +// Retries: Fern retries every request twice by default, POSTs included, which can +// replay non-idempotent calls such as the login or `finances.generateMbReference`. +// Retries are made opt-in: the default becomes 0, while an explicit `maxRetries` +// (per request or in the client options) is still honoured. +import { readFileSync, writeFileSync } from 'node:fs' + +const generated = (path: string) => new URL(`../src/generated/${path}`, import.meta.url) + +// Replaces every occurrence of each `[from, to]` pair in a generated file. +// Fails loudly if a `from` string is missing, so a Fern upgrade that changes the +// generated code cannot silently drop a patch. Already-patched files are accepted +// when `to` is present, which keeps the script idempotent. +function patch(path: string, replacements: [from: string, to: string][]) { + const file = generated(path) + const original = readFileSync(file, 'utf8') + let patched = original + + for (const [from, to] of replacements) { + if (patched.includes(from)) { + patched = patched.replaceAll(from, to) + } else if (!patched.includes(to)) { + console.error(`Expected ${JSON.stringify(from)} not found in ${path}; revisit this patch`) + process.exit(1) + } + } + + if (patched !== original) { + writeFileSync(file, patched) + console.log(`Patched ${path}`) + } +} + +patch('core/fetcher/Headers.ts', [ + ['IterableIterator<[string, string]>', 'HeadersIterator<[string, string]>'], + ['IterableIterator', 'HeadersIterator'], +]) + +patch('core/fetcher/requestWithRetries.ts', [['const DEFAULT_MAX_RETRIES = 2;', 'const DEFAULT_MAX_RETRIES = 0;']]) + +patch('BaseClient.ts', [ + [ + 'The default number of times to retry the request. Defaults to 2.', + 'The default number of times to retry the request. Defaults to 0.', + ], + [ + 'The number of times to retry the request. Defaults to 2.', + 'The number of times to retry the request. Defaults to 0.', + ], +]) + +patch('core/fetcher/makePassthroughRequest.ts', [ + [ + 'Default number of times to retry the request. Defaults to 2.', + 'Default number of times to retry the request. Defaults to 0.', + ], +]) diff --git a/packages/sigarra/src/generated/BaseClient.ts b/packages/sigarra/src/generated/BaseClient.ts new file mode 100644 index 000000000..971f1b1d6 --- /dev/null +++ b/packages/sigarra/src/generated/BaseClient.ts @@ -0,0 +1,63 @@ +// This file was auto-generated by Fern from our API Definition. + +import { mergeHeaders } from "./core/headers.js"; +import * as core from "./core/index.js"; +import type * as environments from "./environments.js"; + +export interface BaseClientOptions { + environment?: core.Supplier; + /** Specify a custom URL to connect the client to. */ + baseUrl?: core.Supplier; + /** Additional headers to include in requests. */ + headers?: Record | null | undefined>; + /** The default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The default number of times to retry the request. Defaults to 0. */ + maxRetries?: number; + /** Provide a custom fetch implementation. Useful for platforms that don't have a built-in fetch or need a custom implementation. */ + fetch?: typeof fetch; + /** Configure logging for the client. */ + logging?: core.logging.LogConfig | core.logging.Logger; + /** Default options for SSE stream reconnection behavior. Has no effect on non-resumable endpoints. */ + stream?: { reconnectionEnabled?: boolean; maxReconnectionAttempts?: number }; +} + +export interface BaseRequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 0. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + /** Additional query string parameters to include in the request. */ + queryParams?: Record; + /** A dictionary containing additional parameters to spread into the request's body. */ + additionalBodyParameters?: Record; + /** Additional headers to include in the request. */ + headers?: Record | null | undefined>; + /** Options for SSE stream reconnection behavior. Has no effect on non-resumable endpoints. */ + stream?: { reconnectionEnabled?: boolean; maxReconnectionAttempts?: number }; +} + +export type NormalizedClientOptions = T & { + logging: core.logging.Logger; +}; + +export function normalizeClientOptions( + options: T, +): NormalizedClientOptions { + const headers = mergeHeaders( + { + "X-Fern-Language": "JavaScript", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + }, + options?.headers, + ); + + return { + ...options, + logging: core.logging.createLogger(options?.logging), + headers, + } as NormalizedClientOptions; +} diff --git a/packages/sigarra/src/generated/Client.ts b/packages/sigarra/src/generated/Client.ts new file mode 100644 index 000000000..58a56cd23 --- /dev/null +++ b/packages/sigarra/src/generated/Client.ts @@ -0,0 +1,121 @@ +// This file was auto-generated by Fern from our API Definition. + +import { AuthClient } from "./api/resources/auth/client/Client.js"; +import { CalendarClient } from "./api/resources/calendar/client/Client.js"; +import { CanteenClient } from "./api/resources/canteen/client/Client.js"; +import { CourseClient } from "./api/resources/course/client/Client.js"; +import { CourseUnitClient } from "./api/resources/courseUnit/client/Client.js"; +import { FinancesClient } from "./api/resources/finances/client/Client.js"; +import { HomeClient } from "./api/resources/home/client/Client.js"; +import { ParkingClient } from "./api/resources/parking/client/Client.js"; +import { PeopleClient } from "./api/resources/people/client/Client.js"; +import { ProfessorClient } from "./api/resources/professor/client/Client.js"; +import { StudentClient } from "./api/resources/student/client/Client.js"; +import { SummariesClient } from "./api/resources/summaries/client/Client.js"; +import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; + +export declare namespace SigarraClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class SigarraClient { + protected readonly _options: NormalizedClientOptions; + protected _student: StudentClient | undefined; + protected _courseUnit: CourseUnitClient | undefined; + protected _summaries: SummariesClient | undefined; + protected _parking: ParkingClient | undefined; + protected _calendar: CalendarClient | undefined; + protected _auth: AuthClient | undefined; + protected _home: HomeClient | undefined; + protected _professor: ProfessorClient | undefined; + protected _people: PeopleClient | undefined; + protected _finances: FinancesClient | undefined; + protected _canteen: CanteenClient | undefined; + protected _course: CourseClient | undefined; + + constructor(options: SigarraClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + public get student(): StudentClient { + return (this._student ??= new StudentClient(this._options)); + } + + public get courseUnit(): CourseUnitClient { + return (this._courseUnit ??= new CourseUnitClient(this._options)); + } + + public get summaries(): SummariesClient { + return (this._summaries ??= new SummariesClient(this._options)); + } + + public get parking(): ParkingClient { + return (this._parking ??= new ParkingClient(this._options)); + } + + public get calendar(): CalendarClient { + return (this._calendar ??= new CalendarClient(this._options)); + } + + public get auth(): AuthClient { + return (this._auth ??= new AuthClient(this._options)); + } + + public get home(): HomeClient { + return (this._home ??= new HomeClient(this._options)); + } + + public get professor(): ProfessorClient { + return (this._professor ??= new ProfessorClient(this._options)); + } + + public get people(): PeopleClient { + return (this._people ??= new PeopleClient(this._options)); + } + + public get finances(): FinancesClient { + return (this._finances ??= new FinancesClient(this._options)); + } + + public get canteen(): CanteenClient { + return (this._canteen ??= new CanteenClient(this._options)); + } + + public get course(): CourseClient { + return (this._course ??= new CourseClient(this._options)); + } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + baseUrl: this._options.baseUrl ?? this._options.environment, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } +} diff --git a/packages/sigarra/src/generated/api/errors/BadRequestError.ts b/packages/sigarra/src/generated/api/errors/BadRequestError.ts new file mode 100644 index 000000000..0b38bd5bf --- /dev/null +++ b/packages/sigarra/src/generated/api/errors/BadRequestError.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../../core/index.js"; +import * as errors from "../../errors/index.js"; + +export class BadRequestError extends errors.SigarraError { + constructor(body?: unknown, rawResponse?: core.RawResponse) { + super({ + message: "BadRequestError", + statusCode: 400, + body: body, + rawResponse: rawResponse, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "BadRequestError"; + } +} diff --git a/packages/sigarra/src/generated/api/errors/ForbiddenError.ts b/packages/sigarra/src/generated/api/errors/ForbiddenError.ts new file mode 100644 index 000000000..387b62d36 --- /dev/null +++ b/packages/sigarra/src/generated/api/errors/ForbiddenError.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../../core/index.js"; +import * as errors from "../../errors/index.js"; + +export class ForbiddenError extends errors.SigarraError { + constructor(body?: unknown, rawResponse?: core.RawResponse) { + super({ + message: "ForbiddenError", + statusCode: 403, + body: body, + rawResponse: rawResponse, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "ForbiddenError"; + } +} diff --git a/packages/sigarra/src/generated/api/errors/UnauthorizedError.ts b/packages/sigarra/src/generated/api/errors/UnauthorizedError.ts new file mode 100644 index 000000000..86701b813 --- /dev/null +++ b/packages/sigarra/src/generated/api/errors/UnauthorizedError.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../../core/index.js"; +import * as errors from "../../errors/index.js"; + +export class UnauthorizedError extends errors.SigarraError { + constructor(body?: unknown, rawResponse?: core.RawResponse) { + super({ + message: "UnauthorizedError", + statusCode: 401, + body: body, + rawResponse: rawResponse, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "UnauthorizedError"; + } +} diff --git a/packages/sigarra/src/generated/api/errors/index.ts b/packages/sigarra/src/generated/api/errors/index.ts new file mode 100644 index 000000000..4733d7b39 --- /dev/null +++ b/packages/sigarra/src/generated/api/errors/index.ts @@ -0,0 +1,3 @@ +export * from "./BadRequestError.js"; +export * from "./ForbiddenError.js"; +export * from "./UnauthorizedError.js"; diff --git a/packages/sigarra/src/generated/api/index.ts b/packages/sigarra/src/generated/api/index.ts new file mode 100644 index 000000000..6ed44b0bf --- /dev/null +++ b/packages/sigarra/src/generated/api/index.ts @@ -0,0 +1,3 @@ +export * from "./errors/index.js"; +export * from "./resources/index.js"; +export * from "./types/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/auth/client/Client.ts b/packages/sigarra/src/generated/api/resources/auth/client/Client.ts new file mode 100644 index 000000000..3a300cbfe --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/Client.ts @@ -0,0 +1,549 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import { mergeAdditionalBodyParameters } from "../../../../core/requestBody.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace AuthClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Logging in (credentials, OpenID Connect, SAML) and out; obtains the session cookies. + */ +export class AuthClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: AuthClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Same as `POST`, with the credentials in the query string. Prefer `POST`: URLs (and so the password) tend to end up in logs. + * + * Used by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper). + * + * @param {Sigarra.LoginWithQueryAuthRequest} request + * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.auth.loginWithQuery({ + * faculty: "feup", + * language: "pt", + * pv_login: "pv_login", + * pv_password: "pv_password" + * }) + */ + public loginWithQuery( + request: Sigarra.LoginWithQueryAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__loginWithQuery(request, requestOptions)); + } + + private async __loginWithQuery( + request: Sigarra.LoginWithQueryAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_login: pvLogin, pv_password: pvPassword } = request; + const _queryParams: Record = { + pv_login: pvLogin, + pv_password: pvPassword, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_val_geral.autentica`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.LoginResult, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_val_geral.autentica", + ); + } + + /** + * Authenticates with a username and password and answers with JSON; on success, sets the session cookies. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.LoginAuthRequest} request + * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.auth.login({ + * faculty: "feup", + * language: "pt", + * pv_login: "pv_login", + * pv_password: "pv_password" + * }) + */ + public login( + request: Sigarra.LoginAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__login(request, requestOptions)); + } + + private async __login( + request: Sigarra.LoginAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): Promise> { + const { faculty, language, ..._body } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_val_geral.autentica`, + ), + method: "POST", + headers: _headers, + contentType: "application/x-www-form-urlencoded", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "form", + body: mergeAdditionalBodyParameters(_body, requestOptions?.additionalBodyParameters), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.LoginResult, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/{faculty}/{language}/mob_val_geral.autentica", + ); + } + + /** + * Authenticates with a username and password; this is the website's login form (served by `vld_validacao.login`). Works on any faculty path, including `up`. + * + * On success, sets `SI_SESSION` and `SI_SECURITY` (no expiry date; the session lasts about 2 hours) and redirects with a `` to `p_address` (`web_page.Inicial` by default). On failure, the reason is in `p.aviso-invalidado` (e.g. "O conjunto utilizador/senha não é válido." for a wrong username or password, "A sua conta encontra-se bloqueada." for a locked account, or an expired password). If the request already carries a valid session, the page is titled "Problema encontrado" ("Problem found"), `p.erro-nota` says "Já se encontra autenticado com o utilizador ." ("You are already authenticated as ."), and no new session is created. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.LoginPageAuthRequest} request + * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.auth.loginPage({ + * faculty: "faculty", + * language: "language", + * p_user: "p_user", + * p_pass: "p_pass" + * }) + */ + public loginPage( + request: Sigarra.LoginPageAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__loginPage(request, requestOptions)); + } + + private async __loginPage( + request: Sigarra.LoginPageAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): Promise> { + const { faculty, language, ..._body } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/vld_validacao.validacao`, + ), + method: "POST", + headers: _headers, + contentType: "application/x-www-form-urlencoded", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "form", + body: mergeAdditionalBodyParameters(_body, requestOptions?.additionalBodyParameters), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/{faculty}/{language}/vld_validacao.validacao", + ); + } + + /** + * Ends the session: its cookies stop working. Then redirects to `web_page.Inicial` with a ``. Answers the same without a session. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.LogoutAuthRequest} request + * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.auth.logout({ + * faculty: "faculty", + * language: "language" + * }) + */ + public logout( + request: Sigarra.LogoutAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__logout(request, requestOptions)); + } + + private async __logout( + request: Sigarra.LogoutAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): Promise> { + const { faculty, language } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/vld_validacao.sair`, + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/vld_validacao.sair", + ); + } + + /** + * Exchanges a token from the U.Porto identity provider (`https://open-id.up.pt/realms/sigarra`) for SIGARRA session cookies. Both access tokens and ID tokens are accepted. The session is bound to the IP address that makes this call, so call it from wherever the session will be used (e.g. a backend, not the browser that obtained the token). Each call creates a new session that lasts 2 hours (`exp` is 7200 s after the request), regardless of the token's own lifetime. [uni](https://github.com/NIAEFEUP/uni)'s tokens have the audience `it-sigarra-oidc-bridge`, likely the name of this service. + * + * Answers `401` without a token and `403` with an invalid one. Only `GET` is allowed (`405` otherwise). + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.OidcTokenAuthRequest} request + * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.UnauthorizedError} + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.auth.oidcToken({ + * Authorization: "Authorization" + * }) + */ + public oidcToken( + request: Sigarra.OidcTokenAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__oidcToken(request, requestOptions)); + } + + private async __oidcToken( + request: Sigarra.OidcTokenAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): Promise> { + const { Authorization: authorization } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + this._options?.headers, + mergeOnlyDefinedHeaders({ Authorization: authorization }), + requestOptions?.headers, + ); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + "auth/oidc/token", + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.OidcTokenAuthResponse, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 401: + throw new Sigarra.UnauthorizedError(_response.error.body as unknown, _response.rawResponse); + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/auth/oidc/token"); + } + + /** + * The login page: a credentials form that posts to `vld_validacao.validacao`, and a federated login form that submits to `vld_validacao.federate_login`. + * + * @param {Sigarra.GetLoginPageAuthRequest} request + * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.auth.getLoginPage({ + * faculty: "faculty", + * language: "language" + * }) + */ + public getLoginPage( + request: Sigarra.GetLoginPageAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getLoginPage(request, requestOptions)); + } + + private async __getLoginPage( + request: Sigarra.GetLoginPageAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): Promise> { + const { faculty, language, p_address: pAddress, p_app: pApp, p_amo: pAmo } = request; + const _queryParams: Record = { + p_address: pAddress, + p_app: pApp, + p_amo: pAmo, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/vld_validacao.login`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/vld_validacao.login", + ); + } + + /** + * Starts the browser login through U.Porto's Shibboleth identity provider: redirects to `https://wayf.up.pt/idp/profile/SAML2/Redirect/SSO` with a SAML `AuthnRequest`. SIGARRA receives the response at `/Shibboleth.sso/SAML2/POST`. + * + * @param {Sigarra.FederatedLoginAuthRequest} request + * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.auth.federatedLogin({ + * faculty: "feup", + * language: "pt", + * p_redirect: "p_redirect" + * }) + */ + public federatedLogin( + request: Sigarra.FederatedLoginAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__federatedLogin(request, requestOptions)); + } + + private async __federatedLogin( + request: Sigarra.FederatedLoginAuthRequest, + requestOptions?: AuthClient.RequestOptions, + ): Promise> { + const { faculty, language, p_redirect: pRedirect } = request; + const _queryParams: Record = { + p_redirect: pRedirect, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/vld_validacao.federate_login`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: undefined, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/vld_validacao.federate_login", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/index.ts b/packages/sigarra/src/generated/api/resources/auth/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/FederatedLoginAuthRequest.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/FederatedLoginAuthRequest.ts new file mode 100644 index 000000000..4e2e1e66a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/FederatedLoginAuthRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * p_redirect: "p_redirect" + * } + */ +export interface FederatedLoginAuthRequest { + faculty: string; + language: string; + /** Page to return to after logging in, e.g. `WEB_PAGE.INICIAL`. */ + p_redirect: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/GetLoginPageAuthRequest.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/GetLoginPageAuthRequest.ts new file mode 100644 index 000000000..f710f63d1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/GetLoginPageAuthRequest.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language" + * } + */ +export interface GetLoginPageAuthRequest { + faculty: string; + language: string; + /** Page to redirect to after logging in, e.g. `WEB_PAGE.INICIAL`. */ + p_address?: string; + p_app?: string; + p_amo?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginAuthRequest.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginAuthRequest.ts new file mode 100644 index 000000000..ed70c3530 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginAuthRequest.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_login: "pv_login", + * pv_password: "pv_password" + * } + */ +export interface LoginAuthRequest { + faculty: string; + language: string; + /** Username, e.g. `up20YYXXXXX`. */ + pv_login: string; + pv_password: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginPageAuthRequest.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginPageAuthRequest.ts new file mode 100644 index 000000000..1a3adcd23 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginPageAuthRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * p_user: "p_user", + * p_pass: "p_pass" + * } + */ +export interface LoginPageAuthRequest { + faculty: string; + language: string; + /** Username, with or without the `up` prefix (e.g. `up20YYXXXXX` or `20YYXXXXX`). */ + p_user: string; + p_pass: string; + /** Page to redirect to after logging in. Defaults to `web_page.Inicial`. */ + p_address?: string; + p_app?: string; + p_amo?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginWithQueryAuthRequest.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginWithQueryAuthRequest.ts new file mode 100644 index 000000000..4627544e1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/LoginWithQueryAuthRequest.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_login: "pv_login", + * pv_password: "pv_password" + * } + */ +export interface LoginWithQueryAuthRequest { + faculty: string; + language: string; + /** Username, e.g. `up20YYXXXXX`. */ + pv_login: string; + pv_password: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/LogoutAuthRequest.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/LogoutAuthRequest.ts new file mode 100644 index 000000000..b40debe77 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/LogoutAuthRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language" + * } + */ +export interface LogoutAuthRequest { + faculty: string; + language: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/OidcTokenAuthRequest.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/OidcTokenAuthRequest.ts new file mode 100644 index 000000000..0bab1e0e5 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/OidcTokenAuthRequest.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * Authorization: "Authorization" + * } + */ +export interface OidcTokenAuthRequest { + /** `Bearer `. */ + Authorization: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/auth/client/requests/index.ts new file mode 100644 index 000000000..961fea521 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/client/requests/index.ts @@ -0,0 +1,7 @@ +export type { FederatedLoginAuthRequest } from "./FederatedLoginAuthRequest.js"; +export type { GetLoginPageAuthRequest } from "./GetLoginPageAuthRequest.js"; +export type { LoginAuthRequest } from "./LoginAuthRequest.js"; +export type { LoginPageAuthRequest } from "./LoginPageAuthRequest.js"; +export type { LoginWithQueryAuthRequest } from "./LoginWithQueryAuthRequest.js"; +export type { LogoutAuthRequest } from "./LogoutAuthRequest.js"; +export type { OidcTokenAuthRequest } from "./OidcTokenAuthRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/auth/exports.ts b/packages/sigarra/src/generated/api/resources/auth/exports.ts new file mode 100644 index 000000000..2e97f0e93 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { AuthClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/auth/index.ts b/packages/sigarra/src/generated/api/resources/auth/index.ts new file mode 100644 index 000000000..d9adb1af9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/index.ts @@ -0,0 +1,2 @@ +export * from "./client/index.js"; +export * from "./types/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/auth/types/OidcTokenAuthResponse.ts b/packages/sigarra/src/generated/api/resources/auth/types/OidcTokenAuthResponse.ts new file mode 100644 index 000000000..b98d253dd --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/types/OidcTokenAuthResponse.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface OidcTokenAuthResponse { + /** When the session cookies expire, as a Unix timestamp: 2 hours after the request. */ + exp: number; + /** `OK` on success. */ + result: string; +} diff --git a/packages/sigarra/src/generated/api/resources/auth/types/index.ts b/packages/sigarra/src/generated/api/resources/auth/types/index.ts new file mode 100644 index 000000000..83622e43b --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/auth/types/index.ts @@ -0,0 +1 @@ +export * from "./OidcTokenAuthResponse.js"; diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/Client.ts b/packages/sigarra/src/generated/api/resources/calendar/client/Client.ts new file mode 100644 index 000000000..83454aa43 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/Client.ts @@ -0,0 +1,551 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace CalendarClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Calendar API (`/calendarios-api/`): schedule events and tables of students, course units and professors. + */ +export class CalendarClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: CalendarClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * The session works from any IP address, not only the one that created it. + * + * A student's schedule, from SIGARRA's new schedule module. + * + * The trailing slash is required: without it SIGARRA redirects to `/calendarios/...`, which returns 404. The URL for a student is in the `data-evt-source-url` and `data-table-source-url` attributes of `hor_geral.estudantes_view`. + * + * @param {Sigarra.GetStudentEventsCalendarRequest} request + * @param {CalendarClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.BadRequestError} + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.calendar.getStudentEvents({ + * faculty: "feup", + * nmec: "nmec", + * academic_year: 1, + * pv_turma_ocorr: ["pv_turma_ocorr"] + * }) + */ + public getStudentEvents( + request: Sigarra.GetStudentEventsCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getStudentEvents(request, requestOptions)); + } + + private async __getStudentEvents( + request: Sigarra.GetStudentEventsCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): Promise> { + const { faculty, nmec, academic_year: academicYear, pv_turma_ocorr: pvTurmaOcorr, period } = request; + const _queryParams: Record = { + academic_year: academicYear, + pv_turma_ocorr: pvTurmaOcorr, + period, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `calendarios-api/api/v1/events/${core.url.encodePathParam(faculty)}/student/${core.url.encodePathParam(nmec)}/`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.CalendarEvents, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Sigarra.BadRequestError(_response.error.body as unknown, _response.rawResponse); + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/calendarios-api/api/v1/events/{faculty}/student/{nmec}/", + ); + } + + /** + * The session works from any IP address, not only the one that created it. + * + * A student's schedule, from SIGARRA's new schedule module, as an HTML table. + * + * The trailing slash is required: without it SIGARRA redirects to `/calendarios/...`, which returns 404. The URL for a student is in the `data-evt-source-url` and `data-table-source-url` attributes of `hor_geral.estudantes_view`. + * + * @param {Sigarra.GetStudentTableCalendarRequest} request + * @param {CalendarClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.BadRequestError} + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.calendar.getStudentTable({ + * faculty: "faculty", + * nmec: "nmec", + * academic_year: 1 + * }) + */ + public getStudentTable( + request: Sigarra.GetStudentTableCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getStudentTable(request, requestOptions)); + } + + private async __getStudentTable( + request: Sigarra.GetStudentTableCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): Promise> { + const { faculty, nmec, academic_year: academicYear, pv_turma_ocorr: pvTurmaOcorr, period } = request; + const _queryParams: Record = { + academic_year: academicYear, + pv_turma_ocorr: pvTurmaOcorr, + period, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `calendarios-api/api/v1/caltable/${core.url.encodePathParam(faculty)}/student/${core.url.encodePathParam(nmec)}/`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Sigarra.BadRequestError(_response.error.body as unknown, _response.rawResponse); + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/calendarios-api/api/v1/caltable/{faculty}/student/{nmec}/", + ); + } + + /** + * The session works from any IP address, not only the one that created it. + * + * The trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetCourseUnitEventsCalendarRequest} request + * @param {CalendarClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.BadRequestError} + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.calendar.getCourseUnitEvents({ + * faculty: "feup", + * ocorr_id: "ocorr_id", + * academic_year: 1, + * period: [1] + * }) + */ + public getCourseUnitEvents( + request: Sigarra.GetCourseUnitEventsCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCourseUnitEvents(request, requestOptions)); + } + + private async __getCourseUnitEvents( + request: Sigarra.GetCourseUnitEventsCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): Promise> { + const { faculty, ocorr_id: ocorrId, academic_year: academicYear, period } = request; + const _queryParams: Record = { + academic_year: academicYear, + period, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `calendarios-api/api/v1/events/${core.url.encodePathParam(faculty)}/uc/${core.url.encodePathParam(ocorrId)}/`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.CalendarEvents, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Sigarra.BadRequestError(_response.error.body as unknown, _response.rawResponse); + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/calendarios-api/api/v1/events/{faculty}/uc/{ocorr_id}/", + ); + } + + /** + * The session works from any IP address, not only the one that created it. + * + * The trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetCourseUnitTableCalendarRequest} request + * @param {CalendarClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.BadRequestError} + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.calendar.getCourseUnitTable({ + * faculty: "faculty", + * ocorr_id: "ocorr_id", + * academic_year: 1 + * }) + */ + public getCourseUnitTable( + request: Sigarra.GetCourseUnitTableCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCourseUnitTable(request, requestOptions)); + } + + private async __getCourseUnitTable( + request: Sigarra.GetCourseUnitTableCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): Promise> { + const { faculty, ocorr_id: ocorrId, academic_year: academicYear, period } = request; + const _queryParams: Record = { + academic_year: academicYear, + period, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `calendarios-api/api/v1/caltable/${core.url.encodePathParam(faculty)}/uc/${core.url.encodePathParam(ocorrId)}/`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Sigarra.BadRequestError(_response.error.body as unknown, _response.rawResponse); + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/calendarios-api/api/v1/caltable/{faculty}/uc/{ocorr_id}/", + ); + } + + /** + * The session works from any IP address, not only the one that created it. + * + * The trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetProfessorEventsCalendarRequest} request + * @param {CalendarClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.BadRequestError} + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.calendar.getProfessorEvents({ + * faculty: "feup", + * doc_codigo: "doc_codigo", + * academic_year: 1, + * period: [1] + * }) + */ + public getProfessorEvents( + request: Sigarra.GetProfessorEventsCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getProfessorEvents(request, requestOptions)); + } + + private async __getProfessorEvents( + request: Sigarra.GetProfessorEventsCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): Promise> { + const { faculty, doc_codigo: docCodigo, academic_year: academicYear, period } = request; + const _queryParams: Record = { + academic_year: academicYear, + period, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `calendarios-api/api/v1/events/${core.url.encodePathParam(faculty)}/doc/${core.url.encodePathParam(docCodigo)}/`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.CalendarEvents, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Sigarra.BadRequestError(_response.error.body as unknown, _response.rawResponse); + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/calendarios-api/api/v1/events/{faculty}/doc/{doc_codigo}/", + ); + } + + /** + * The session works from any IP address, not only the one that created it. + * + * The trailing slash is required. The URL is in the `data-evt-source-url` / `data-table-source-url` attributes of the corresponding `hor_geral.*_view` page. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetProfessorTableCalendarRequest} request + * @param {CalendarClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.BadRequestError} + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.calendar.getProfessorTable({ + * faculty: "faculty", + * doc_codigo: "doc_codigo", + * academic_year: 1 + * }) + */ + public getProfessorTable( + request: Sigarra.GetProfessorTableCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getProfessorTable(request, requestOptions)); + } + + private async __getProfessorTable( + request: Sigarra.GetProfessorTableCalendarRequest, + requestOptions?: CalendarClient.RequestOptions, + ): Promise> { + const { faculty, doc_codigo: docCodigo, academic_year: academicYear, period } = request; + const _queryParams: Record = { + academic_year: academicYear, + period, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `calendarios-api/api/v1/caltable/${core.url.encodePathParam(faculty)}/doc/${core.url.encodePathParam(docCodigo)}/`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Sigarra.BadRequestError(_response.error.body as unknown, _response.rawResponse); + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/calendarios-api/api/v1/caltable/{faculty}/doc/{doc_codigo}/", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/index.ts b/packages/sigarra/src/generated/api/resources/calendar/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetCourseUnitEventsCalendarRequest.ts b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetCourseUnitEventsCalendarRequest.ts new file mode 100644 index 000000000..954df76c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetCourseUnitEventsCalendarRequest.ts @@ -0,0 +1,20 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * ocorr_id: "ocorr_id", + * academic_year: 1, + * period: [1] + * } + */ +export interface GetCourseUnitEventsCalendarRequest { + faculty: string; + /** Course unit occurrence ID. */ + ocorr_id: string; + /** First year of the academic year, e.g. `2023` for 2023/2024. */ + academic_year: number; + /** Teaching periods to include. `uni` sends 1–8. */ + period?: number | number[]; +} diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetCourseUnitTableCalendarRequest.ts b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetCourseUnitTableCalendarRequest.ts new file mode 100644 index 000000000..cb0fa8499 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetCourseUnitTableCalendarRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * ocorr_id: "ocorr_id", + * academic_year: 1 + * } + */ +export interface GetCourseUnitTableCalendarRequest { + faculty: string; + /** Course unit occurrence ID. */ + ocorr_id: string; + /** First year of the academic year, e.g. `2023` for 2023/2024. */ + academic_year: number; + /** Teaching periods to include. `uni` sends 1–8. */ + period?: number | number[]; +} diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetProfessorEventsCalendarRequest.ts b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetProfessorEventsCalendarRequest.ts new file mode 100644 index 000000000..249e5020b --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetProfessorEventsCalendarRequest.ts @@ -0,0 +1,20 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * doc_codigo: "doc_codigo", + * academic_year: 1, + * period: [1] + * } + */ +export interface GetProfessorEventsCalendarRequest { + faculty: string; + /** Professor code. */ + doc_codigo: string; + /** First year of the academic year, e.g. `2023` for 2023/2024. */ + academic_year: number; + /** Teaching periods to include. `uni` sends 1–8. */ + period?: number | number[]; +} diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetProfessorTableCalendarRequest.ts b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetProfessorTableCalendarRequest.ts new file mode 100644 index 000000000..fc67e4cf6 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetProfessorTableCalendarRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * doc_codigo: "doc_codigo", + * academic_year: 1 + * } + */ +export interface GetProfessorTableCalendarRequest { + faculty: string; + /** Professor code. */ + doc_codigo: string; + /** First year of the academic year, e.g. `2023` for 2023/2024. */ + academic_year: number; + /** Teaching periods to include. `uni` sends 1–8. */ + period?: number | number[]; +} diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetStudentEventsCalendarRequest.ts b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetStudentEventsCalendarRequest.ts new file mode 100644 index 000000000..a8de8f6f4 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetStudentEventsCalendarRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * nmec: "nmec", + * academic_year: 1, + * pv_turma_ocorr: ["pv_turma_ocorr"] + * } + */ +export interface GetStudentEventsCalendarRequest { + faculty: string; + /** Student number. */ + nmec: string; + /** First year of the academic year, e.g. `2023` for 2023/2024. */ + academic_year: number; + /** Classes to include, as `{FACULTY}_{ocorr_id}_{turma_id}` (e.g. `FEUP_518805_213039`). At least one is required. */ + pv_turma_ocorr?: string | string[]; + /** Teaching periods to include. */ + period?: number | number[]; +} diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetStudentTableCalendarRequest.ts b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetStudentTableCalendarRequest.ts new file mode 100644 index 000000000..0bc055a60 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/requests/GetStudentTableCalendarRequest.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * nmec: "nmec", + * academic_year: 1 + * } + */ +export interface GetStudentTableCalendarRequest { + faculty: string; + /** Student number. */ + nmec: string; + /** First year of the academic year, e.g. `2023` for 2023/2024. */ + academic_year: number; + /** Classes to include, as `{FACULTY}_{ocorr_id}_{turma_id}` (e.g. `FEUP_518805_213039`). At least one is required. */ + pv_turma_ocorr?: string | string[]; + /** Teaching periods to include. */ + period?: number | number[]; +} diff --git a/packages/sigarra/src/generated/api/resources/calendar/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/calendar/client/requests/index.ts new file mode 100644 index 000000000..fee0dc4b4 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/client/requests/index.ts @@ -0,0 +1,6 @@ +export type { GetCourseUnitEventsCalendarRequest } from "./GetCourseUnitEventsCalendarRequest.js"; +export type { GetCourseUnitTableCalendarRequest } from "./GetCourseUnitTableCalendarRequest.js"; +export type { GetProfessorEventsCalendarRequest } from "./GetProfessorEventsCalendarRequest.js"; +export type { GetProfessorTableCalendarRequest } from "./GetProfessorTableCalendarRequest.js"; +export type { GetStudentEventsCalendarRequest } from "./GetStudentEventsCalendarRequest.js"; +export type { GetStudentTableCalendarRequest } from "./GetStudentTableCalendarRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/calendar/exports.ts b/packages/sigarra/src/generated/api/resources/calendar/exports.ts new file mode 100644 index 000000000..3cc572f93 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { CalendarClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/calendar/index.ts b/packages/sigarra/src/generated/api/resources/calendar/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/calendar/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/canteen/client/Client.ts b/packages/sigarra/src/generated/api/resources/canteen/client/Client.ts new file mode 100644 index 000000000..8267a0ed5 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/canteen/client/Client.ts @@ -0,0 +1,94 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import type * as Sigarra from "../../../index.js"; + +export declare namespace CanteenClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Canteen menus. + */ +export class CanteenClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: CanteenClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Canteen menus. `uni` requests it on `feup`. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetMenusPageCanteenRequest} request + * @param {CanteenClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.canteen.getMenusPage({ + * faculty: "faculty", + * language: "language" + * }) + */ + public getMenusPage( + request: Sigarra.GetMenusPageCanteenRequest, + requestOptions?: CanteenClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getMenusPage(request, requestOptions)); + } + + private async __getMenusPage( + request: Sigarra.GetMenusPageCanteenRequest, + requestOptions?: CanteenClient.RequestOptions, + ): Promise> { + const { faculty, language } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/CANTINA.EMENTASHOW`, + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/CANTINA.EMENTASHOW", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/canteen/client/index.ts b/packages/sigarra/src/generated/api/resources/canteen/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/canteen/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/canteen/client/requests/GetMenusPageCanteenRequest.ts b/packages/sigarra/src/generated/api/resources/canteen/client/requests/GetMenusPageCanteenRequest.ts new file mode 100644 index 000000000..3572091e1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/canteen/client/requests/GetMenusPageCanteenRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language" + * } + */ +export interface GetMenusPageCanteenRequest { + faculty: string; + language: string; +} diff --git a/packages/sigarra/src/generated/api/resources/canteen/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/canteen/client/requests/index.ts new file mode 100644 index 000000000..9c2a2f9f9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/canteen/client/requests/index.ts @@ -0,0 +1 @@ +export type { GetMenusPageCanteenRequest } from "./GetMenusPageCanteenRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/canteen/exports.ts b/packages/sigarra/src/generated/api/resources/canteen/exports.ts new file mode 100644 index 000000000..beff8e10c --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/canteen/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { CanteenClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/canteen/index.ts b/packages/sigarra/src/generated/api/resources/canteen/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/canteen/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/course/client/Client.ts b/packages/sigarra/src/generated/api/resources/course/client/Client.ts new file mode 100644 index 000000000..caa1e76c1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/client/Client.ts @@ -0,0 +1,273 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import type * as Sigarra from "../../../index.js"; + +export declare namespace CourseClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Degree programme pages: study plan and class vacancies. + */ +export class CourseClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: CourseClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * A course page: name, acronym (the "Sigla:" / "Acronym:" row), and a "Planos de Estudos" (study plans) section linking to `cur_geral.cur_planos_estudos_view`. An unknown course answers `200` with a "Sem resultados" (no results) page. + * + * Used by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper). + * + * @param {Sigarra.GetPageCourseRequest} request + * @param {CourseClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.course.getPage({ + * faculty: "faculty", + * language: "language", + * pv_curso_id: "pv_curso_id" + * }) + */ + public getPage( + request: Sigarra.GetPageCourseRequest, + requestOptions?: CourseClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getPage(request, requestOptions)); + } + + private async __getPage( + request: Sigarra.GetPageCourseRequest, + requestOptions?: CourseClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_curso_id: pvCursoId, + pv_ano_lectivo: pvAnoLectivo, + pv_origem: pvOrigem, + pv_tipo_cur_sigla: pvTipoCurSigla, + } = request; + const _queryParams: Record = { + pv_curso_id: pvCursoId, + pv_ano_lectivo: pvAnoLectivo, + pv_origem: pvOrigem, + pv_tipo_cur_sigla: pvTipoCurSigla, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/cur_geral.cur_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/cur_geral.cur_view", + ); + } + + /** + * A course's study plan. Each group of course units is a `div[id^="div_id_"]` with an `h3` title and a `table.dadossz` whose rows link to `ucurr_geral.ficha_uc_view?pv_ocorrencia_id=`. + * + * Used by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper). + * + * @param {Sigarra.GetStudyPlanPageCourseRequest} request + * @param {CourseClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.course.getStudyPlanPage({ + * faculty: "faculty", + * language: "language", + * pv_plano_id: "pv_plano_id", + * pv_ano_lectivo: "pv_ano_lectivo" + * }) + */ + public getStudyPlanPage( + request: Sigarra.GetStudyPlanPageCourseRequest, + requestOptions?: CourseClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getStudyPlanPage(request, requestOptions)); + } + + private async __getStudyPlanPage( + request: Sigarra.GetStudyPlanPageCourseRequest, + requestOptions?: CourseClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_plano_id: pvPlanoId, + pv_ano_lectivo: pvAnoLectivo, + pv_tipo_cur_sigla: pvTipoCurSigla, + pv_origem: pvOrigem, + } = request; + const _queryParams: Record = { + pv_plano_id: pvPlanoId, + pv_ano_lectivo: pvAnoLectivo, + pv_tipo_cur_sigla: pvTipoCurSigla, + pv_origem: pvOrigem, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/cur_geral.cur_planos_estudos_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/cur_geral.cur_planos_estudos_view", + ); + } + + /** + * Vacancies in each class of a course's course units, for the current class enrolment period (e.g. "Inscrições nas turmas 1º Semestre - 2026/2027", the 1st semester's class enrolments). The data is a table with one row per course unit and pairs of columns (class, vacancies). The course doesn't need to belong to the faculty in the path. + * + * Used by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper). + * + * @param {Sigarra.GetClassVacanciesPageCourseRequest} request + * @param {CourseClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.course.getClassVacanciesPage({ + * faculty: "faculty", + * language: "language" + * }) + */ + public getClassVacanciesPage( + request: Sigarra.GetClassVacanciesPageCourseRequest, + requestOptions?: CourseClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getClassVacanciesPage(request, requestOptions)); + } + + private async __getClassVacanciesPage( + request: Sigarra.GetClassVacanciesPageCourseRequest, + requestOptions?: CourseClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_curso_id: pvCursoId, pv_colunas: pvColunas } = request; + const _queryParams: Record = { + pv_curso_id: pvCursoId, + pv_colunas: pvColunas, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/it_geral.vagas`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/it_geral.vagas", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/course/client/index.ts b/packages/sigarra/src/generated/api/resources/course/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/course/client/requests/GetClassVacanciesPageCourseRequest.ts b/packages/sigarra/src/generated/api/resources/course/client/requests/GetClassVacanciesPageCourseRequest.ts new file mode 100644 index 000000000..a82ca6b47 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/client/requests/GetClassVacanciesPageCourseRequest.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language" + * } + */ +export interface GetClassVacanciesPageCourseRequest { + faculty: string; + language: string; + /** Course ID. Without it, the page has no table. */ + pv_curso_id?: string; + /** How many class columns per row, `3` to `9` (default `8`). */ + pv_colunas?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/course/client/requests/GetPageCourseRequest.ts b/packages/sigarra/src/generated/api/resources/course/client/requests/GetPageCourseRequest.ts new file mode 100644 index 000000000..3c331101b --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/client/requests/GetPageCourseRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_curso_id: "pv_curso_id" + * } + */ +export interface GetPageCourseRequest { + faculty: string; + language: string; + /** Course ID. */ + pv_curso_id: string; + /** Academic year, e.g. `2023`. Defaults to the current one. */ + pv_ano_lectivo?: string; + /** Where the link came from, e.g. `CUR`. Doesn't change the content. */ + pv_origem?: string; + /** Course type: `A` refresher course, `B` pre-Bologna bachelor's degree (*bacharelato*), `CF` complementary training course, `CFC` continuing education course, `VI` summer/winter course, `FL` free course or training, `D` doctorate, `E` specialisation, `EA` advanced studies, `EG` EUGLOH, `EIA` European Innovation Academy, `FC` continuing education, `L` bachelor's degree (*licenciatura*), `LV` free, `M` master's degree, `MI` integrated master's degree, `PD` postdoctoral programme, `UFC` continuing education unit, `UM` unit/module/training action. */ + pv_tipo_cur_sigla?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/course/client/requests/GetStudyPlanPageCourseRequest.ts b/packages/sigarra/src/generated/api/resources/course/client/requests/GetStudyPlanPageCourseRequest.ts new file mode 100644 index 000000000..5c1f9a8ab --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/client/requests/GetStudyPlanPageCourseRequest.ts @@ -0,0 +1,23 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_plano_id: "pv_plano_id", + * pv_ano_lectivo: "pv_ano_lectivo" + * } + */ +export interface GetStudyPlanPageCourseRequest { + faculty: string; + language: string; + /** Study plan ID, from the course page. */ + pv_plano_id: string; + /** Academic year, e.g. `2023`. */ + pv_ano_lectivo: string; + /** Course type: `A` refresher course, `B` pre-Bologna bachelor's degree (*bacharelato*), `CF` complementary training course, `CFC` continuing education course, `VI` summer/winter course, `FL` free course or training, `D` doctorate, `E` specialisation, `EA` advanced studies, `EG` EUGLOH, `EIA` European Innovation Academy, `FC` continuing education, `L` bachelor's degree (*licenciatura*), `LV` free, `M` master's degree, `MI` integrated master's degree, `PD` postdoctoral programme, `UFC` continuing education unit, `UM` unit/module/training action. */ + pv_tipo_cur_sigla?: string; + /** Where the link came from, e.g. `CUR`. Doesn't change the content. */ + pv_origem?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/course/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/course/client/requests/index.ts new file mode 100644 index 000000000..5daab9bfe --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/client/requests/index.ts @@ -0,0 +1,3 @@ +export type { GetClassVacanciesPageCourseRequest } from "./GetClassVacanciesPageCourseRequest.js"; +export type { GetPageCourseRequest } from "./GetPageCourseRequest.js"; +export type { GetStudyPlanPageCourseRequest } from "./GetStudyPlanPageCourseRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/course/exports.ts b/packages/sigarra/src/generated/api/resources/course/exports.ts new file mode 100644 index 000000000..c313ea881 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { CourseClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/course/index.ts b/packages/sigarra/src/generated/api/resources/course/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/course/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/Client.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/Client.ts new file mode 100644 index 000000000..719ff9371 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/Client.ts @@ -0,0 +1,1091 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace CourseUnitClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Course units (UCs): information, classes, enrolled students, files, occurrences, schedules and exams. + */ +export class CourseUnitClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: CourseUnitClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Lists the occurrences of a course unit, given one of its occurrences. + * + * @param {Sigarra.GetOccurrencesCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getOccurrences({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * }) + */ + public getOccurrences( + request: Sigarra.GetOccurrencesCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getOccurrences(request, requestOptions)); + } + + private async __getOccurrences( + request: Sigarra.GetOccurrencesCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.outras_ocorrencias`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as Sigarra.GetOccurrencesCourseUnitResponseItem[], + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.outras_ocorrencias", + ); + } + + /** + * Given an occurrence ID, returns the contents of its course unit. + * + * @param {Sigarra.GetInfoCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getInfo({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * }) + */ + public getInfo( + request: Sigarra.GetInfoCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getInfo(request, requestOptions)); + } + + private async __getInfo( + request: Sigarra.GetInfoCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.perfil`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.GetInfoCourseUnitResponse, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.perfil", + ); + } + + /** + * Lists the people enrolled in a course unit. + * + * @param {Sigarra.GetEnrolledCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getEnrolled({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * }) + */ + public getEnrolled( + request: Sigarra.GetEnrolledCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getEnrolled(request, requestOptions)); + } + + private async __getEnrolled( + request: Sigarra.GetEnrolledCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.uc_inscritos`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as Sigarra.GetEnrolledCourseUnitResponseItem[], + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.uc_inscritos", + ); + } + + /** + * For some occurrences (e.g. 2nd-semester ones), SIGARRA answers 200 with a malformed body: a leaked `Status: 404` CGI header followed by `{"erro", "erro_msg"}`. + * + * @param {Sigarra.GetClassesCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getClasses({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * }) + */ + public getClasses( + request: Sigarra.GetClassesCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getClasses(request, requestOptions)); + } + + private async __getClasses( + request: Sigarra.GetClassesCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.uc_turmas`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.GetClassesCourseUnitResponse, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.uc_turmas", + ); + } + + /** + * Files (contents) of a course unit. + * + * @param {Sigarra.GetFilesCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getFiles({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484400" + * }) + */ + public getFiles( + request: Sigarra.GetFilesCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getFiles(request, requestOptions)); + } + + private async __getFiles( + request: Sigarra.GetFilesCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.conteudos`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as Sigarra.GetFilesCourseUnitResponseItem[], + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.conteudos", + ); + } + + /** + * Without a valid session, SIGARRA answers `200` with an HTML error page ("Erro!") instead of the file. + * + * Downloads a file by its ID. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + */ + public downloadFile( + request: Sigarra.DownloadFileCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__downloadFile(request, requestOptions)); + } + + private async __downloadFile( + request: Sigarra.DownloadFileCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pct_id: pctId } = request; + const _queryParams: Record = { + pct_id: pctId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/conteudos_service.conteudos_cont`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "binary-response", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/conteudos_service.conteudos_cont", + ); + } + + /** + * Exam calendar of a course unit occurrence; exams are in `td.exame` cells. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetExamsPageCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getExamsPage({ + * faculty: "faculty", + * language: "language", + * p_ocorr_id: "p_ocorr_id" + * }) + */ + public getExamsPage( + request: Sigarra.GetExamsPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getExamsPage(request, requestOptions)); + } + + private async __getExamsPage( + request: Sigarra.GetExamsPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, p_ocorr_id: pOcorrId } = request; + const _queryParams: Record = { + p_ocorr_id: pOcorrId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/exa_geral.mapa_de_exames`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/exa_geral.mapa_de_exames", + ); + } + + /** + * A course unit's schedule page. `#cal-shadow-container[data-evt-source-url]` has the URL for `calendar.getCourseUnitEvents`. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetSchedulePageCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getSchedulePage({ + * faculty: "faculty", + * language: "language", + * pv_ocorrencia_id: "pv_ocorrencia_id", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodos: "pv_periodos" + * }) + */ + public getSchedulePage( + request: Sigarra.GetSchedulePageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getSchedulePage(request, requestOptions)); + } + + private async __getSchedulePage( + request: Sigarra.GetSchedulePageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_ocorrencia_id: pvOcorrenciaId, + pv_ano_lectivo: pvAnoLectivo, + pv_periodos: pvPeriodos, + } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + pv_ano_lectivo: pvAnoLectivo, + pv_periodos: pvPeriodos, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/hor_geral.ucurr_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/hor_geral.ucurr_view", + ); + } + + /** + * Lists the courses a course unit occurrence belongs to, with links to `it_listagem.lista_turma_disciplina` for each. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetCoursesPageCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getCoursesPage({ + * faculty: "faculty", + * language: "language", + * pv_ocorrencia_id: "pv_ocorrencia_id" + * }) + */ + public getCoursesPage( + request: Sigarra.GetCoursesPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCoursesPage(request, requestOptions)); + } + + private async __getCoursesPage( + request: Sigarra.GetCoursesPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/it_listagem.lista_cursos_disciplina`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/it_listagem.lista_cursos_disciplina", + ); + } + + /** + * Lists a course unit's classes and their students, for one course. `uni` follows the links in `it_listagem.lista_cursos_disciplina`. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetClassesPageCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getClassesPage({ + * faculty: "faculty", + * language: "language", + * pv_curso_id: "pv_curso_id", + * pv_ocorrencia_id: "pv_ocorrencia_id", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodo_id: "pv_periodo_id" + * }) + */ + public getClassesPage( + request: Sigarra.GetClassesPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getClassesPage(request, requestOptions)); + } + + private async __getClassesPage( + request: Sigarra.GetClassesPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_curso_id: pvCursoId, + pv_ocorrencia_id: pvOcorrenciaId, + pv_ano_lectivo: pvAnoLectivo, + pv_periodo_id: pvPeriodoId, + pv_no_menu: pvNoMenu, + } = request; + const _queryParams: Record = { + pv_curso_id: pvCursoId, + pv_ocorrencia_id: pvOcorrenciaId, + pv_ano_lectivo: pvAnoLectivo, + pv_periodo_id: pvPeriodoId, + pv_no_menu: pvNoMenu, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/it_listagem.lista_turma_disciplina`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/it_listagem.lista_turma_disciplina", + ); + } + + /** + * Weekly timetable of a course unit occurrence. Returns the weekly timetable of the week that contains `pv_semana_ini`; `pv_semana_fim` does not widen the range. + * + * @param {Sigarra.GetScheduleCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getSchedule({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "pv_ocorrencia_id", + * pv_semana_ini: "pv_semana_ini", + * pv_semana_fim: "pv_semana_fim" + * }) + */ + public getSchedule( + request: Sigarra.GetScheduleCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getSchedule(request, requestOptions)); + } + + private async __getSchedule( + request: Sigarra.GetScheduleCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_ocorrencia_id: pvOcorrenciaId, + pv_semana_ini: pvSemanaIni, + pv_semana_fim: pvSemanaFim, + } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + pv_semana_ini: pvSemanaIni, + pv_semana_fim: pvSemanaFim, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_hor_geral.ucurr`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.WeeklySchedule, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_hor_geral.ucurr", + ); + } + + /** + * Search results for course unit occurrences (the form is `ucurr_geral.pesquisa_ucs`). Each result row (`table.dados .d`) links to `ucurr_geral.ficha_uc_view?pv_ocorrencia_id=`; results are paginated, and the last page is linked from `.paginar-saltar-barra-posicao`. All filters are optional. + * + * Used by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper). + * + * @param {Sigarra.SearchPageCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.searchPage({ + * faculty: "faculty", + * language: "language" + * }) + */ + public searchPage( + request: Sigarra.SearchPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__searchPage(request, requestOptions)); + } + + private async __searchPage( + request: Sigarra.SearchPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_ano_lectivo: pvAnoLectivo, + pv_curso_id: pvCursoId, + pv_curso_nome: pvCursoNome, + pv_uc_codigo: pvUcCodigo, + pv_uc_sigla: pvUcSigla, + pv_uc_nome: pvUcNome, + pv_uc_ano: pvUcAno, + pv_uc_periodo: pvUcPeriodo, + pv_doc_codigo: pvDocCodigo, + pv_doc_sigla: pvDocSigla, + pv_doc_nome: pvDocNome, + pv_lingua: pvLingua, + pv_tipo_curso: pvTipoCurso, + pv_tipo_avaliacao: pvTipoAvaliacao, + pv_num_pag: pvNumPag, + } = request; + const _queryParams: Record = { + pv_ano_lectivo: pvAnoLectivo, + pv_curso_id: pvCursoId, + pv_curso_nome: pvCursoNome, + pv_uc_codigo: pvUcCodigo, + pv_uc_sigla: pvUcSigla, + pv_uc_nome: pvUcNome, + pv_uc_ano: pvUcAno, + pv_uc_periodo: pvUcPeriodo, + pv_doc_codigo: pvDocCodigo, + pv_doc_sigla: pvDocSigla, + pv_doc_nome: pvDocNome, + pv_lingua: pvLingua, + pv_tipo_curso: pvTipoCurso, + pv_tipo_avaliacao: pvTipoAvaliacao, + pv_num_pag: pvNumPag, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/ucurr_geral.pesquisa_ocorr_ucs_list`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/ucurr_geral.pesquisa_ocorr_ucs_list", + ); + } + + /** + * A course unit occurrence's page: name, acronym ("Sigla:" / "Acronym:"), occurrence (`h2`, e.g. "Ocorrência: 2023/2024 - 1S"), and the "Ciclos de Estudo/Cursos" (study cycles/courses) table (course, curricular year, ECTS). With a valid session it also shows teaching responsibilities and hours, and navigation links (contents, students, schedule, classes, exams, …); in particular, the schedule link, `Horário`, links to `hor_geral.ucurr_view?pv_ocorrencia_id=` only with a session. An unknown occurrence answers `200` with a "Sem Resultados" (no results) page. + * + * Used by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper). + * + * @param {Sigarra.GetSheetPageCourseUnitRequest} request + * @param {CourseUnitClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.courseUnit.getSheetPage({ + * faculty: "faculty", + * language: "language", + * pv_ocorrencia_id: "pv_ocorrencia_id" + * }) + */ + public getSheetPage( + request: Sigarra.GetSheetPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getSheetPage(request, requestOptions)); + } + + private async __getSheetPage( + request: Sigarra.GetSheetPageCourseUnitRequest, + requestOptions?: CourseUnitClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/ucurr_geral.ficha_uc_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/ucurr_geral.ficha_uc_view", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/index.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/DownloadFileCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/DownloadFileCourseUnitRequest.ts new file mode 100644 index 000000000..6071459a4 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/DownloadFileCourseUnitRequest.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface DownloadFileCourseUnitRequest { + faculty: string; + language: string; + /** The id of the document to be downloaded */ + pct_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetClassesCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetClassesCourseUnitRequest.ts new file mode 100644 index 000000000..2b7cb3122 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetClassesCourseUnitRequest.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * } + */ +export interface GetClassesCourseUnitRequest { + faculty: string; + language: string; + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetClassesPageCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetClassesPageCourseUnitRequest.ts new file mode 100644 index 000000000..4c1e18e71 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetClassesPageCourseUnitRequest.ts @@ -0,0 +1,27 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_curso_id: "pv_curso_id", + * pv_ocorrencia_id: "pv_ocorrencia_id", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodo_id: "pv_periodo_id" + * } + */ +export interface GetClassesPageCourseUnitRequest { + faculty: string; + language: string; + /** Course ID. */ + pv_curso_id: string; + /** Course unit occurrence ID. */ + pv_ocorrencia_id: string; + /** Academic year, e.g. `2023`. */ + pv_ano_lectivo: string; + /** Period ID. */ + pv_periodo_id: string; + /** Set to `1` to omit the page menu. */ + pv_no_menu?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetCoursesPageCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetCoursesPageCourseUnitRequest.ts new file mode 100644 index 000000000..6cc141ba1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetCoursesPageCourseUnitRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_ocorrencia_id: "pv_ocorrencia_id" + * } + */ +export interface GetCoursesPageCourseUnitRequest { + faculty: string; + language: string; + /** Course unit occurrence ID. */ + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetEnrolledCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetEnrolledCourseUnitRequest.ts new file mode 100644 index 000000000..725607273 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetEnrolledCourseUnitRequest.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * } + */ +export interface GetEnrolledCourseUnitRequest { + faculty: string; + language: string; + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetExamsPageCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetExamsPageCourseUnitRequest.ts new file mode 100644 index 000000000..4524ffc8f --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetExamsPageCourseUnitRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * p_ocorr_id: "p_ocorr_id" + * } + */ +export interface GetExamsPageCourseUnitRequest { + faculty: string; + language: string; + /** Course unit occurrence ID. */ + p_ocorr_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetFilesCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetFilesCourseUnitRequest.ts new file mode 100644 index 000000000..36d8cb37d --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetFilesCourseUnitRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484400" + * } + */ +export interface GetFilesCourseUnitRequest { + faculty: string; + language: string; + /** ID of the occurrence of the course unit whose files to get. */ + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetInfoCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetInfoCourseUnitRequest.ts new file mode 100644 index 000000000..736923771 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetInfoCourseUnitRequest.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * } + */ +export interface GetInfoCourseUnitRequest { + faculty: string; + language: string; + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetOccurrencesCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetOccurrencesCourseUnitRequest.ts new file mode 100644 index 000000000..27a0c90fc --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetOccurrencesCourseUnitRequest.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484377" + * } + */ +export interface GetOccurrencesCourseUnitRequest { + faculty: string; + language: string; + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetScheduleCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetScheduleCourseUnitRequest.ts new file mode 100644 index 000000000..bfff70177 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetScheduleCourseUnitRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "pv_ocorrencia_id", + * pv_semana_ini: "pv_semana_ini", + * pv_semana_fim: "pv_semana_fim" + * } + */ +export interface GetScheduleCourseUnitRequest { + faculty: string; + language: string; + /** Course unit occurrence ID. */ + pv_ocorrencia_id: string; + /** Start of the period, as YYYYMMDD. */ + pv_semana_ini: string; + /** End of the period, as YYYYMMDD. */ + pv_semana_fim: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetSchedulePageCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetSchedulePageCourseUnitRequest.ts new file mode 100644 index 000000000..87aecb4ea --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetSchedulePageCourseUnitRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_ocorrencia_id: "pv_ocorrencia_id", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodos: "pv_periodos" + * } + */ +export interface GetSchedulePageCourseUnitRequest { + faculty: string; + language: string; + /** Course unit occurrence ID. */ + pv_ocorrencia_id: string; + /** Academic year, e.g. `2023`. */ + pv_ano_lectivo: string; + /** `uni` sends `1`. */ + pv_periodos: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetSheetPageCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetSheetPageCourseUnitRequest.ts new file mode 100644 index 000000000..adfbc357e --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/GetSheetPageCourseUnitRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_ocorrencia_id: "pv_ocorrencia_id" + * } + */ +export interface GetSheetPageCourseUnitRequest { + faculty: string; + language: string; + /** Course unit occurrence ID. */ + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/SearchPageCourseUnitRequest.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/SearchPageCourseUnitRequest.ts new file mode 100644 index 000000000..2fa08593e --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/SearchPageCourseUnitRequest.ts @@ -0,0 +1,43 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language" + * } + */ +export interface SearchPageCourseUnitRequest { + faculty: string; + language: string; + /** Academic year, e.g. `2023`. */ + pv_ano_lectivo?: string; + /** Course ID. */ + pv_curso_id?: string; + /** Course name. */ + pv_curso_nome?: string; + /** Course unit code, e.g. `M.EIC001`. */ + pv_uc_codigo?: string; + /** Course unit acronym, e.g. `AC`. */ + pv_uc_sigla?: string; + /** Course unit name. */ + pv_uc_nome?: string; + /** Curricular year. */ + pv_uc_ano?: string; + /** Period: `1` annual, `2` 1st semester, `3` 2nd semester, `4`–`7` 1st–4th trimester, `8` no period. */ + pv_uc_periodo?: string; + /** Teacher code. */ + pv_doc_codigo?: string; + /** Teacher acronym. */ + pv_doc_sigla?: string; + /** Teacher name. */ + pv_doc_nome?: string; + /** Teaching language. */ + pv_lingua?: string; + /** Course type: `A` refresher course, `B` pre-Bologna bachelor's degree (*bacharelato*), `CF` complementary training course, `CFC` continuing education course, `VI` summer/winter course, `FL` free course or training, `D` doctorate, `E` specialisation, `EA` advanced studies, `EG` EUGLOH, `EIA` European Innovation Academy, `FC` continuing education, `L` bachelor's degree (*licenciatura*), `LV` free, `M` master's degree, `MI` integrated master's degree, `PD` postdoctoral programme, `UFC` continuing education unit, `UM` unit/module/training action. */ + pv_tipo_curso?: string; + /** Assessment type. */ + pv_tipo_avaliacao?: string; + /** Page number, from `1`. */ + pv_num_pag?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/index.ts new file mode 100644 index 000000000..d82b15751 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/client/requests/index.ts @@ -0,0 +1,13 @@ +export type { DownloadFileCourseUnitRequest } from "./DownloadFileCourseUnitRequest.js"; +export type { GetClassesCourseUnitRequest } from "./GetClassesCourseUnitRequest.js"; +export type { GetClassesPageCourseUnitRequest } from "./GetClassesPageCourseUnitRequest.js"; +export type { GetCoursesPageCourseUnitRequest } from "./GetCoursesPageCourseUnitRequest.js"; +export type { GetEnrolledCourseUnitRequest } from "./GetEnrolledCourseUnitRequest.js"; +export type { GetExamsPageCourseUnitRequest } from "./GetExamsPageCourseUnitRequest.js"; +export type { GetFilesCourseUnitRequest } from "./GetFilesCourseUnitRequest.js"; +export type { GetInfoCourseUnitRequest } from "./GetInfoCourseUnitRequest.js"; +export type { GetOccurrencesCourseUnitRequest } from "./GetOccurrencesCourseUnitRequest.js"; +export type { GetScheduleCourseUnitRequest } from "./GetScheduleCourseUnitRequest.js"; +export type { GetSchedulePageCourseUnitRequest } from "./GetSchedulePageCourseUnitRequest.js"; +export type { GetSheetPageCourseUnitRequest } from "./GetSheetPageCourseUnitRequest.js"; +export type { SearchPageCourseUnitRequest } from "./SearchPageCourseUnitRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/exports.ts b/packages/sigarra/src/generated/api/resources/courseUnit/exports.ts new file mode 100644 index 000000000..506c46f2d --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { CourseUnitClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/index.ts b/packages/sigarra/src/generated/api/resources/courseUnit/index.ts new file mode 100644 index 000000000..d9adb1af9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/index.ts @@ -0,0 +1,2 @@ +export * from "./client/index.js"; +export * from "./types/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/types/GetClassesCourseUnitResponse.ts b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetClassesCourseUnitResponse.ts new file mode 100644 index 000000000..84e41eb2b --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetClassesCourseUnitResponse.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetClassesCourseUnitResponse { + turmas: GetClassesCourseUnitResponse.Turmas.Item[]; +} + +export namespace GetClassesCourseUnitResponse { + export type Turmas = Turmas.Item[]; + + export namespace Turmas { + export interface Item { + turma_id: number; + n_max_alunos: unknown | null; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/types/GetEnrolledCourseUnitResponseItem.ts b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetEnrolledCourseUnitResponseItem.ts new file mode 100644 index 000000000..3ec160149 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetEnrolledCourseUnitResponseItem.ts @@ -0,0 +1,6 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetEnrolledCourseUnitResponseItem { + codigo: string; + nome: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/types/GetFilesCourseUnitResponseItem.ts b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetFilesCourseUnitResponseItem.ts new file mode 100644 index 000000000..63149b32c --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetFilesCourseUnitResponseItem.ts @@ -0,0 +1,27 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetFilesCourseUnitResponseItem { + codigo: number; + nome: string; + name: string; + nivel: number; + ficheiros: GetFilesCourseUnitResponseItem.Ficheiros.Item[]; +} + +export namespace GetFilesCourseUnitResponseItem { + export type Ficheiros = Ficheiros.Item[]; + + export namespace Ficheiros { + export interface Item { + codigo: number; + nome: string; + tipo: string; + url: string; + filename: string; + tamanho: number; + data_actualizacao: string; + comentario: string; + descricao: string; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/types/GetInfoCourseUnitResponse.ts b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetInfoCourseUnitResponse.ts new file mode 100644 index 000000000..0f73f8c7d --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetInfoCourseUnitResponse.ts @@ -0,0 +1,135 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetInfoCourseUnitResponse { + codigo: string; + ucurr_id: number; + nome: string; + name: string; + sigla: string; + areas: GetInfoCourseUnitResponse.Areas.Item[]; + ano_lectivo: number; + pagina_elearning: string; + carga_horaria: GetInfoCourseUnitResponse.CargaHoraria.Item[]; + ds: GetInfoCourseUnitResponse.Ds.Item[]; + responsabilidades: GetInfoCourseUnitResponse.Responsabilidades.Item[]; + estado?: string | undefined; + lingua?: string | undefined; + objectivos?: string | undefined; + conteudo?: string | undefined; + bibliografia?: GetInfoCourseUnitResponse.Bibliografia.Item[] | undefined; + bib_obs?: string | undefined; + metodologia?: string | undefined; + avaliacao?: string | undefined; + software?: GetInfoCourseUnitResponse.Software.Item[] | undefined; + keywords?: GetInfoCourseUnitResponse.Keywords.Item[] | undefined; + modo_avaliacao?: string | undefined; + comp_avaliacao?: GetInfoCourseUnitResponse.CompAvaliacao.Item[] | undefined; + cond_frequencia?: string | undefined; + for_avaliacao?: string | undefined; + provas_avaliacao?: string | undefined; + forma_avaliacao?: string | undefined; + forma_melhoria?: string | undefined; + observacoes?: string | undefined; + pagina_web?: string | undefined; +} + +export namespace GetInfoCourseUnitResponse { + export type Areas = Areas.Item[]; + + export namespace Areas { + export interface Item { + nome: string; + name: string; + } + } + + export type CargaHoraria = CargaHoraria.Item[]; + + export namespace CargaHoraria { + export interface Item { + tipo: string; + descricao: string; + horas: number | null; + } + } + + export type Ds = Ds.Item[]; + + export namespace Ds { + export interface Item { + tipo: string; + tipo_descricao: string; + num_turmas: number; + num_horas: number | null; + docentes: Item.Docentes.Item[]; + } + + export namespace Item { + export type Docentes = Docentes.Item[]; + + export namespace Docentes { + export interface Item { + doc_codigo: string; + nome: string; + horas: number; + } + } + } + } + + export type Responsabilidades = Responsabilidades.Item[]; + + export namespace Responsabilidades { + export interface Item { + codigo: string; + nome: string; + papel: string; + } + } + + export type Bibliografia = Bibliografia.Item[]; + + export namespace Bibliografia { + export interface Item { + tipo: string; + tipo_descr: string; + autores: string; + titulo: string; + link: string; + editor: string; + ano: string; + isbn: string; + obs: string; + } + } + + export type Software = Software.Item[]; + + export namespace Software { + export interface Item { + nome: unknown | null; + descricao: string; + } + } + + export type Keywords = Keywords.Item[]; + + export namespace Keywords { + export interface Item { + descricao: string; + } + } + + export type CompAvaliacao = CompAvaliacao.Item[]; + + export namespace CompAvaliacao { + export interface Item { + descricao: string; + tipo: string; + tipo_descr: string; + duracao: number | null; + data_conclusao: string; + peso: number | null; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/types/GetOccurrencesCourseUnitResponseItem.ts b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetOccurrencesCourseUnitResponseItem.ts new file mode 100644 index 000000000..bb5ce19f2 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/types/GetOccurrencesCourseUnitResponseItem.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetOccurrencesCourseUnitResponseItem { + id: number; + nome: string; + name: string; + ano_letivo: number; + data_inicio: string; + data_fim: string; + periodo_nome: string; + periodo_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/courseUnit/types/index.ts b/packages/sigarra/src/generated/api/resources/courseUnit/types/index.ts new file mode 100644 index 000000000..c5df2a001 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/courseUnit/types/index.ts @@ -0,0 +1,5 @@ +export * from "./GetClassesCourseUnitResponse.js"; +export * from "./GetEnrolledCourseUnitResponseItem.js"; +export * from "./GetFilesCourseUnitResponseItem.js"; +export * from "./GetInfoCourseUnitResponse.js"; +export * from "./GetOccurrencesCourseUnitResponseItem.js"; diff --git a/packages/sigarra/src/generated/api/resources/finances/client/Client.ts b/packages/sigarra/src/generated/api/resources/finances/client/Client.ts new file mode 100644 index 000000000..45046148d --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/client/Client.ts @@ -0,0 +1,264 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import { mergeAdditionalBodyParameters } from "../../../../core/requestBody.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace FinancesClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Current account, printing balance and Multibanco payment references. + */ +export class FinancesClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: FinancesClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * A person's current account: unpaid fees, payment references and the account statement. `uni` also requests it on the `sasup` faculty for social services. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetCurrentAccountPageFinancesRequest} request + * @param {FinancesClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.finances.getCurrentAccountPage({ + * faculty: "faculty", + * language: "language", + * pct_cod: "pct_cod" + * }) + */ + public getCurrentAccountPage( + request: Sigarra.GetCurrentAccountPageFinancesRequest, + requestOptions?: FinancesClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCurrentAccountPage(request, requestOptions)); + } + + private async __getCurrentAccountPage( + request: Sigarra.GetCurrentAccountPageFinancesRequest, + requestOptions?: FinancesClient.RequestOptions, + ): Promise> { + const { faculty, language, pct_cod: pctCod } = request; + const _queryParams: Record = { + pct_cod: pctCod, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/gpag_ccorrente_geral.conta_corrente_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/gpag_ccorrente_geral.conta_corrente_view", + ); + } + + /** + * Printing balance and history. `uni` requests it on `feup`. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetPrintingPageFinancesRequest} request + * @param {FinancesClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.finances.getPrintingPage({ + * faculty: "faculty", + * language: "language", + * p_codigo: "p_codigo" + * }) + */ + public getPrintingPage( + request: Sigarra.GetPrintingPageFinancesRequest, + requestOptions?: FinancesClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getPrintingPage(request, requestOptions)); + } + + private async __getPrintingPage( + request: Sigarra.GetPrintingPageFinancesRequest, + requestOptions?: FinancesClient.RequestOptions, + ): Promise> { + const { faculty, language, p_codigo: pCodigo } = request; + const _queryParams: Record = { + p_codigo: pCodigo, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/imp4_impressoes.atribs`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/imp4_impressoes.atribs", + ); + } + + /** + * Creates a Multibanco payment reference, e.g. to top up the printing balance. **This creates a real payment reference.** Not verified live. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GenerateMbReferenceFinancesRequest} request + * @param {FinancesClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.finances.generateMbReference({ + * faculty: "faculty", + * language: "language", + * p_tipo_id: "p_tipo_id", + * pct_codigo: "pct_codigo", + * p_valor: "p_valor", + * p_valor_livre: "p_valor_livre" + * }) + */ + public generateMbReference( + request: Sigarra.GenerateMbReferenceFinancesRequest, + requestOptions?: FinancesClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__generateMbReference(request, requestOptions)); + } + + private async __generateMbReference( + request: Sigarra.GenerateMbReferenceFinancesRequest, + requestOptions?: FinancesClient.RequestOptions, + ): Promise> { + const { faculty, language, ..._body } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/gpag_ccorrentes_geral.gerar_mb`, + ), + method: "POST", + headers: _headers, + contentType: "application/x-www-form-urlencoded", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "form", + body: mergeAdditionalBodyParameters(_body, requestOptions?.additionalBodyParameters), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/{faculty}/{language}/gpag_ccorrentes_geral.gerar_mb", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/finances/client/index.ts b/packages/sigarra/src/generated/api/resources/finances/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/finances/client/requests/GenerateMbReferenceFinancesRequest.ts b/packages/sigarra/src/generated/api/resources/finances/client/requests/GenerateMbReferenceFinancesRequest.ts new file mode 100644 index 000000000..e6616dcf1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/client/requests/GenerateMbReferenceFinancesRequest.ts @@ -0,0 +1,25 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * p_tipo_id: "p_tipo_id", + * pct_codigo: "pct_codigo", + * p_valor: "p_valor", + * p_valor_livre: "p_valor_livre" + * } + */ +export interface GenerateMbReferenceFinancesRequest { + faculty: string; + language: string; + /** Reference type. `uni` sends `3` (printing). */ + p_tipo_id: string; + /** Username. */ + pct_codigo: string; + /** `uni` sends `1`. */ + p_valor: string; + /** Amount, with a decimal comma (e.g. `5,00`). Minimum 1,00 €. */ + p_valor_livre: string; +} diff --git a/packages/sigarra/src/generated/api/resources/finances/client/requests/GetCurrentAccountPageFinancesRequest.ts b/packages/sigarra/src/generated/api/resources/finances/client/requests/GetCurrentAccountPageFinancesRequest.ts new file mode 100644 index 000000000..7a0b835ab --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/client/requests/GetCurrentAccountPageFinancesRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pct_cod: "pct_cod" + * } + */ +export interface GetCurrentAccountPageFinancesRequest { + faculty: string; + language: string; + /** Username. */ + pct_cod: string; +} diff --git a/packages/sigarra/src/generated/api/resources/finances/client/requests/GetPrintingPageFinancesRequest.ts b/packages/sigarra/src/generated/api/resources/finances/client/requests/GetPrintingPageFinancesRequest.ts new file mode 100644 index 000000000..48d771892 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/client/requests/GetPrintingPageFinancesRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * p_codigo: "p_codigo" + * } + */ +export interface GetPrintingPageFinancesRequest { + faculty: string; + language: string; + /** Username. */ + p_codigo: string; +} diff --git a/packages/sigarra/src/generated/api/resources/finances/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/finances/client/requests/index.ts new file mode 100644 index 000000000..624d8c9d4 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/client/requests/index.ts @@ -0,0 +1,3 @@ +export type { GenerateMbReferenceFinancesRequest } from "./GenerateMbReferenceFinancesRequest.js"; +export type { GetCurrentAccountPageFinancesRequest } from "./GetCurrentAccountPageFinancesRequest.js"; +export type { GetPrintingPageFinancesRequest } from "./GetPrintingPageFinancesRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/finances/exports.ts b/packages/sigarra/src/generated/api/resources/finances/exports.ts new file mode 100644 index 000000000..fd13c31d9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { FinancesClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/finances/index.ts b/packages/sigarra/src/generated/api/resources/finances/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/finances/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/home/client/Client.ts b/packages/sigarra/src/generated/api/resources/home/client/Client.ts new file mode 100644 index 000000000..1e8b26cb6 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/home/client/Client.ts @@ -0,0 +1,174 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace HomeClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Faculty home page and the list of faculties. + */ +export class HomeClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: HomeClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Home page. When authenticated, `.autenticado .fotografia img` links to `fotografias_service.foto?pct_cod=`, which `uni` uses to find the username. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetHomePageHomeRequest} request + * @param {HomeClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.home.getHomePage({ + * faculty: "faculty", + * language: "language" + * }) + */ + public getHomePage( + request: Sigarra.GetHomePageHomeRequest, + requestOptions?: HomeClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getHomePage(request, requestOptions)); + } + + private async __getHomePage( + request: Sigarra.GetHomePageHomeRequest, + requestOptions?: HomeClient.RequestOptions, + ): Promise> { + const { faculty, language } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/web_page.inicial`, + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/web_page.inicial", + ); + } + + /** + * Institutional page generator. Only `/up/pt/web_base.gera_pagina?p_pagina=escolas` (the list of faculties) still answers: it redirects (`301`) to `https://www.up.pt/portal/pt/conhecer/sobre-a-uporto/as-faculdades/`. Any other combination (another faculty, another page, or no `p_pagina`) is refused by the web server with a generic `403`. + * + * Used by [uporto-schedule-scrapper](https://github.com/NIAEFEUP/uporto-schedule-scrapper). + * + * @param {Sigarra.GetFacultiesRedirectHomeRequest} request + * @param {HomeClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.home.getFacultiesRedirect({ + * faculty: "feup", + * language: "pt", + * p_pagina: "p_pagina" + * }) + */ + public getFacultiesRedirect( + request: Sigarra.GetFacultiesRedirectHomeRequest, + requestOptions?: HomeClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getFacultiesRedirect(request, requestOptions)); + } + + private async __getFacultiesRedirect( + request: Sigarra.GetFacultiesRedirectHomeRequest, + requestOptions?: HomeClient.RequestOptions, + ): Promise> { + const { faculty, language, p_pagina: pPagina } = request; + const _queryParams: Record = { + p_pagina: pPagina, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/web_base.gera_pagina`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: undefined, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/web_base.gera_pagina", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/home/client/index.ts b/packages/sigarra/src/generated/api/resources/home/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/home/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/home/client/requests/GetFacultiesRedirectHomeRequest.ts b/packages/sigarra/src/generated/api/resources/home/client/requests/GetFacultiesRedirectHomeRequest.ts new file mode 100644 index 000000000..e72e9d44f --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/home/client/requests/GetFacultiesRedirectHomeRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * p_pagina: "p_pagina" + * } + */ +export interface GetFacultiesRedirectHomeRequest { + faculty: string; + language: string; + /** Page name. Only `escolas` works (with `faculty` = `up`). */ + p_pagina: string; +} diff --git a/packages/sigarra/src/generated/api/resources/home/client/requests/GetHomePageHomeRequest.ts b/packages/sigarra/src/generated/api/resources/home/client/requests/GetHomePageHomeRequest.ts new file mode 100644 index 000000000..65327d947 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/home/client/requests/GetHomePageHomeRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language" + * } + */ +export interface GetHomePageHomeRequest { + faculty: string; + language: string; +} diff --git a/packages/sigarra/src/generated/api/resources/home/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/home/client/requests/index.ts new file mode 100644 index 000000000..52fd11c26 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/home/client/requests/index.ts @@ -0,0 +1,2 @@ +export type { GetFacultiesRedirectHomeRequest } from "./GetFacultiesRedirectHomeRequest.js"; +export type { GetHomePageHomeRequest } from "./GetHomePageHomeRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/home/exports.ts b/packages/sigarra/src/generated/api/resources/home/exports.ts new file mode 100644 index 000000000..d1ef65dc7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/home/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { HomeClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/home/index.ts b/packages/sigarra/src/generated/api/resources/home/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/home/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/index.ts b/packages/sigarra/src/generated/api/resources/index.ts new file mode 100644 index 000000000..56b6f9b82 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/index.ts @@ -0,0 +1,29 @@ +export * from "./auth/client/requests/index.js"; +export * as auth from "./auth/index.js"; +export * from "./auth/types/index.js"; +export * from "./calendar/client/requests/index.js"; +export * as calendar from "./calendar/index.js"; +export * from "./canteen/client/requests/index.js"; +export * as canteen from "./canteen/index.js"; +export * from "./course/client/requests/index.js"; +export * as course from "./course/index.js"; +export * from "./courseUnit/client/requests/index.js"; +export * as courseUnit from "./courseUnit/index.js"; +export * from "./courseUnit/types/index.js"; +export * from "./finances/client/requests/index.js"; +export * as finances from "./finances/index.js"; +export * from "./home/client/requests/index.js"; +export * as home from "./home/index.js"; +export * from "./parking/client/requests/index.js"; +export * as parking from "./parking/index.js"; +export * from "./parking/types/index.js"; +export * from "./people/client/requests/index.js"; +export * as people from "./people/index.js"; +export * from "./professor/client/requests/index.js"; +export * as professor from "./professor/index.js"; +export * from "./student/client/requests/index.js"; +export * as student from "./student/index.js"; +export * from "./student/types/index.js"; +export * from "./summaries/client/requests/index.js"; +export * as summaries from "./summaries/index.js"; +export * from "./summaries/types/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/parking/client/Client.ts b/packages/sigarra/src/generated/api/resources/parking/client/Client.ts new file mode 100644 index 000000000..90430f250 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/client/Client.ts @@ -0,0 +1,89 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import type * as Sigarra from "../../../index.js"; + +export declare namespace ParkingClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Parking lot occupancy. + */ +export class ParkingClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: ParkingClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * @param {Sigarra.GetOccupancyParkingRequest} request + * @param {ParkingClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.parking.getOccupancy({ + * faculty: "feup", + * language: "pt" + * }) + */ + public getOccupancy( + request: Sigarra.GetOccupancyParkingRequest, + requestOptions?: ParkingClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getOccupancy(request, requestOptions)); + } + + private async __getOccupancy( + request: Sigarra.GetOccupancyParkingRequest, + requestOptions?: ParkingClient.RequestOptions, + ): Promise> { + const { faculty, language } = request; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/instalacs_geral.ocupacao_parques`, + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.GetOccupancyParkingResponse, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/instalacs_geral.ocupacao_parques", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/parking/client/index.ts b/packages/sigarra/src/generated/api/resources/parking/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/parking/client/requests/GetOccupancyParkingRequest.ts b/packages/sigarra/src/generated/api/resources/parking/client/requests/GetOccupancyParkingRequest.ts new file mode 100644 index 000000000..cfad4fa99 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/client/requests/GetOccupancyParkingRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt" + * } + */ +export interface GetOccupancyParkingRequest { + faculty: string; + language: string; +} diff --git a/packages/sigarra/src/generated/api/resources/parking/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/parking/client/requests/index.ts new file mode 100644 index 000000000..24f3e4589 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/client/requests/index.ts @@ -0,0 +1 @@ +export type { GetOccupancyParkingRequest } from "./GetOccupancyParkingRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/parking/exports.ts b/packages/sigarra/src/generated/api/resources/parking/exports.ts new file mode 100644 index 000000000..215494c85 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { ParkingClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/parking/index.ts b/packages/sigarra/src/generated/api/resources/parking/index.ts new file mode 100644 index 000000000..d9adb1af9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/index.ts @@ -0,0 +1,2 @@ +export * from "./client/index.js"; +export * from "./types/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/parking/types/GetOccupancyParkingResponse.ts b/packages/sigarra/src/generated/api/resources/parking/types/GetOccupancyParkingResponse.ts new file mode 100644 index 000000000..ae034f40e --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/types/GetOccupancyParkingResponse.ts @@ -0,0 +1,42 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetOccupancyParkingResponse { + itdc: GetOccupancyParkingResponse.Itdc.Item[]; +} + +export namespace GetOccupancyParkingResponse { + export type Itdc = Itdc.Item[]; + + export namespace Itdc { + export interface Item { + versao: string; + dataresposta: string; + codigoresposta: string; + statementsql: string; + linhas: string; + erro: string; + resposta: Item.Resposta; + } + + export namespace Item { + export interface Resposta { + p1lotacao: number; + p3lotacao: number; + p4lotacao: number; + data: string; + P1in: number; + P1out: number; + P3in: number; + P3out: number; + P4in: number; + P4out: number; + p1ocupados: number; + p3ocupados: number; + p4ocupados: number; + p1livres: number; + p3livres: number; + p4livres: number; + } + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/parking/types/index.ts b/packages/sigarra/src/generated/api/resources/parking/types/index.ts new file mode 100644 index 000000000..759fd8346 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/parking/types/index.ts @@ -0,0 +1 @@ +export * from "./GetOccupancyParkingResponse.js"; diff --git a/packages/sigarra/src/generated/api/resources/people/client/Client.ts b/packages/sigarra/src/generated/api/resources/people/client/Client.ts new file mode 100644 index 000000000..3635abaa8 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/people/client/Client.ts @@ -0,0 +1,94 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import type * as Sigarra from "../../../index.js"; + +export declare namespace PeopleClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * People-related resources, such as photographs. + */ +export class PeopleClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: PeopleClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Without a valid session, SIGARRA answers `200` with a placeholder image. + * + * Photo of a student or staff member. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + */ + public getPhoto( + request: Sigarra.GetPhotoPeopleRequest, + requestOptions?: PeopleClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getPhoto(request, requestOptions)); + } + + private async __getPhoto( + request: Sigarra.GetPhotoPeopleRequest, + requestOptions?: PeopleClient.RequestOptions, + ): Promise> { + const { faculty, language, pct_cod: pctCod } = request; + const _queryParams: Record = { + pct_cod: pctCod, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/fotografias_service.foto`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "binary-response", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/fotografias_service.foto", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/people/client/index.ts b/packages/sigarra/src/generated/api/resources/people/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/people/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/people/client/requests/GetPhotoPeopleRequest.ts b/packages/sigarra/src/generated/api/resources/people/client/requests/GetPhotoPeopleRequest.ts new file mode 100644 index 000000000..6acde9300 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/people/client/requests/GetPhotoPeopleRequest.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetPhotoPeopleRequest { + faculty: string; + language: string; + /** Student number or staff code. */ + pct_cod: string; +} diff --git a/packages/sigarra/src/generated/api/resources/people/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/people/client/requests/index.ts new file mode 100644 index 000000000..11fe68019 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/people/client/requests/index.ts @@ -0,0 +1 @@ +export type { GetPhotoPeopleRequest } from "./GetPhotoPeopleRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/people/exports.ts b/packages/sigarra/src/generated/api/resources/people/exports.ts new file mode 100644 index 000000000..4ea557e42 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/people/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { PeopleClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/people/index.ts b/packages/sigarra/src/generated/api/resources/people/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/people/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/professor/client/Client.ts b/packages/sigarra/src/generated/api/resources/professor/client/Client.ts new file mode 100644 index 000000000..074afdec3 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/professor/client/Client.ts @@ -0,0 +1,195 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace ProfessorClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Professor pages and schedules. + */ +export class ProfessorClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: ProfessorClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Public, but shows more information with a valid session. + * + * A staff member's page. `uni` extracts the institutional email (from `mailto:` links or an obfuscated `onclick`) and offices (`instal_geral.espaco_view` links). + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetProfilePageProfessorRequest} request + * @param {ProfessorClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.professor.getProfilePage({ + * faculty: "faculty", + * language: "language", + * p_codigo: "p_codigo" + * }) + */ + public getProfilePage( + request: Sigarra.GetProfilePageProfessorRequest, + requestOptions?: ProfessorClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getProfilePage(request, requestOptions)); + } + + private async __getProfilePage( + request: Sigarra.GetProfilePageProfessorRequest, + requestOptions?: ProfessorClient.RequestOptions, + ): Promise> { + const { faculty, language, p_codigo: pCodigo } = request; + const _queryParams: Record = { + p_codigo: pCodigo, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/func_geral.formview`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/func_geral.formview", + ); + } + + /** + * A professor's schedule page. `#cal-shadow-container[data-evt-source-url]` has the URL for `calendar.getProfessorEvents`. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetSchedulePageProfessorRequest} request + * @param {ProfessorClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.professor.getSchedulePage({ + * faculty: "faculty", + * language: "language", + * pv_doc_codigo: "pv_doc_codigo", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodos: "pv_periodos" + * }) + */ + public getSchedulePage( + request: Sigarra.GetSchedulePageProfessorRequest, + requestOptions?: ProfessorClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getSchedulePage(request, requestOptions)); + } + + private async __getSchedulePage( + request: Sigarra.GetSchedulePageProfessorRequest, + requestOptions?: ProfessorClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_doc_codigo: pvDocCodigo, + pv_ano_lectivo: pvAnoLectivo, + pv_periodos: pvPeriodos, + } = request; + const _queryParams: Record = { + pv_doc_codigo: pvDocCodigo, + pv_ano_lectivo: pvAnoLectivo, + pv_periodos: pvPeriodos, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/hor_geral.docentes_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/hor_geral.docentes_view", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/professor/client/index.ts b/packages/sigarra/src/generated/api/resources/professor/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/professor/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/professor/client/requests/GetProfilePageProfessorRequest.ts b/packages/sigarra/src/generated/api/resources/professor/client/requests/GetProfilePageProfessorRequest.ts new file mode 100644 index 000000000..9ba4ea22b --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/professor/client/requests/GetProfilePageProfessorRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * p_codigo: "p_codigo" + * } + */ +export interface GetProfilePageProfessorRequest { + faculty: string; + language: string; + /** Staff code. */ + p_codigo: string; +} diff --git a/packages/sigarra/src/generated/api/resources/professor/client/requests/GetSchedulePageProfessorRequest.ts b/packages/sigarra/src/generated/api/resources/professor/client/requests/GetSchedulePageProfessorRequest.ts new file mode 100644 index 000000000..58e94af46 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/professor/client/requests/GetSchedulePageProfessorRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_doc_codigo: "pv_doc_codigo", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodos: "pv_periodos" + * } + */ +export interface GetSchedulePageProfessorRequest { + faculty: string; + language: string; + /** Professor code. */ + pv_doc_codigo: string; + /** Academic year, e.g. `2023`. */ + pv_ano_lectivo: string; + /** `uni` sends `1`. */ + pv_periodos: string; +} diff --git a/packages/sigarra/src/generated/api/resources/professor/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/professor/client/requests/index.ts new file mode 100644 index 000000000..f5b2a976a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/professor/client/requests/index.ts @@ -0,0 +1,2 @@ +export type { GetProfilePageProfessorRequest } from "./GetProfilePageProfessorRequest.js"; +export type { GetSchedulePageProfessorRequest } from "./GetSchedulePageProfessorRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/professor/exports.ts b/packages/sigarra/src/generated/api/resources/professor/exports.ts new file mode 100644 index 000000000..46e7ae3a6 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/professor/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { ProfessorClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/professor/index.ts b/packages/sigarra/src/generated/api/resources/professor/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/professor/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/student/client/Client.ts b/packages/sigarra/src/generated/api/resources/student/client/Client.ts new file mode 100644 index 000000000..e13d13c13 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/Client.ts @@ -0,0 +1,934 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace StudentClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Student schedule, exams, academic path, profile, courses and enrolments. + */ +export class StudentClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: StudentClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Weekly timetable of a student. Returns the weekly timetable of the week that contains `pv_semana_ini`; `pv_semana_fim` does not widen the range. + * + * @param {Sigarra.GetScheduleStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getSchedule({ + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo", + * pv_semana_ini: "pv_semana_ini", + * pv_semana_fim: "pv_semana_fim" + * }) + */ + public getSchedule( + request: Sigarra.GetScheduleStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getSchedule(request, requestOptions)); + } + + private async __getSchedule( + request: Sigarra.GetScheduleStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_codigo: pvCodigo, + pv_semana_ini: pvSemanaIni, + pv_semana_fim: pvSemanaFim, + } = request; + const _queryParams: Record = { + pv_codigo: pvCodigo, + pv_semana_ini: pvSemanaIni, + pv_semana_fim: pvSemanaFim, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_hor_geral.estudante`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.WeeklySchedule, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_hor_geral.estudante", + ); + } + + /** + * The authenticated user's exams. + * + * @param {Sigarra.GetExamsStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getExams({ + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * }) + */ + public getExams( + request: Sigarra.GetExamsStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getExams(request, requestOptions)); + } + + private async __getExams( + request: Sigarra.GetExamsStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_codigo: pvCodigo } = request; + const _queryParams: Record = { + pv_codigo: pvCodigo, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_fest_geral.exames`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as Sigarra.GetExamsStudentResponseItem[], + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_fest_geral.exames", + ); + } + + /** + * The authenticated user's academic path. + * + * @param {Sigarra.GetAcademicPathStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getAcademicPath({ + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * }) + */ + public getAcademicPath( + request: Sigarra.GetAcademicPathStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getAcademicPath(request, requestOptions)); + } + + private async __getAcademicPath( + request: Sigarra.GetAcademicPathStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_codigo: pvCodigo, + pv_ano_lectivo_min: pvAnoLectivoMin, + pv_ano_lectivo_max: pvAnoLectivoMax, + } = request; + const _queryParams: Record = { + pv_codigo: pvCodigo, + pv_ano_lectivo_min: pvAnoLectivoMin, + pv_ano_lectivo_max: pvAnoLectivoMax, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_fest_geral.percurso_academico`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as Sigarra.GetAcademicPathStudentResponseItem[], + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_fest_geral.percurso_academico", + ); + } + + /** + * The authenticated user's details. + * + * @param {Sigarra.GetProfileStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getProfile({ + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * }) + */ + public getProfile( + request: Sigarra.GetProfileStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getProfile(request, requestOptions)); + } + + private async __getProfile( + request: Sigarra.GetProfileStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_codigo: pvCodigo } = request; + const _queryParams: Record = { + pv_codigo: pvCodigo, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_fest_geral.perfil`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.GetProfileStudentResponse, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_fest_geral.perfil", + ); + } + + /** + * Lists the courses the student is enrolled in, with their current course units. + * + * @param {Sigarra.GetCurrentCoursesStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getCurrentCourses({ + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * }) + */ + public getCurrentCourses( + request: Sigarra.GetCurrentCoursesStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCurrentCourses(request, requestOptions)); + } + + private async __getCurrentCourses( + request: Sigarra.GetCurrentCoursesStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_codigo: pvCodigo } = request; + const _queryParams: Record = { + pv_codigo: pvCodigo, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_fest_geral.ucurr_inscricoes_corrente`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as Sigarra.GetCurrentCoursesStudentResponseItem[], + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_fest_geral.ucurr_inscricoes_corrente", + ); + } + + /** + * Without a valid session, SIGARRA answers `200` with a redirect page to another faculty instead of the list. + * + * Lists the faculties a person belongs to (`#conteudoinner>ul a`, e.g. "Faculdade de Engenharia (FEUP)"). `uni` requests it with `faculty` set to `up`. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetFacultiesPageStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getFacultiesPage({ + * faculty: "faculty", + * language: "language", + * pct_codigo: "pct_codigo" + * }) + */ + public getFacultiesPage( + request: Sigarra.GetFacultiesPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getFacultiesPage(request, requestOptions)); + } + + private async __getFacultiesPage( + request: Sigarra.GetFacultiesPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pct_codigo: pctCodigo } = request; + const _queryParams: Record = { + pct_codigo: pctCodigo, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/vld_entidades_geral.entidade_pagina`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/vld_entidades_geral.entidade_pagina", + ); + } + + /** + * Lists a student's courses and links to each `fest_geral.curso_percurso_academico_view?pv_fest_id=`. `uni` also uses it to check whether the session is still valid. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetCoursesPageStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getCoursesPage({ + * faculty: "faculty", + * language: "language", + * pv_num_unico: "pv_num_unico" + * }) + */ + public getCoursesPage( + request: Sigarra.GetCoursesPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCoursesPage(request, requestOptions)); + } + + private async __getCoursesPage( + request: Sigarra.GetCoursesPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_num_unico: pvNumUnico } = request; + const _queryParams: Record = { + pv_num_unico: pvNumUnico, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/fest_geral.cursos_list`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/fest_geral.cursos_list", + ); + } + + /** + * A student's academic path in a course: course units, grades and the course average. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetCourseAcademicPathPageStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getCourseAcademicPathPage({ + * faculty: "faculty", + * language: "language", + * pv_fest_id: "pv_fest_id" + * }) + */ + public getCourseAcademicPathPage( + request: Sigarra.GetCourseAcademicPathPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCourseAcademicPathPage(request, requestOptions)); + } + + private async __getCourseAcademicPathPage( + request: Sigarra.GetCourseAcademicPathPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_fest_id: pvFestId } = request; + const _queryParams: Record = { + pv_fest_id: pvFestId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/fest_geral.curso_percurso_academico_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/fest_geral.curso_percurso_academico_view", + ); + } + + /** + * A student's course unit enrolments in a course. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetCourseUnitEnrollmentsPageStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getCourseUnitEnrollmentsPage({ + * faculty: "faculty", + * language: "language", + * pv_fest_id: "pv_fest_id" + * }) + */ + public getCourseUnitEnrollmentsPage( + request: Sigarra.GetCourseUnitEnrollmentsPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getCourseUnitEnrollmentsPage(request, requestOptions)); + } + + private async __getCourseUnitEnrollmentsPage( + request: Sigarra.GetCourseUnitEnrollmentsPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_fest_id: pvFestId } = request; + const _queryParams: Record = { + pv_fest_id: pvFestId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/fest_geral.ucurr_inscricoes_list`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/fest_geral.ucurr_inscricoes_list", + ); + } + + /** + * Without a valid session, SIGARRA answers `200` with a one-line error ("Erro - Não tem permissões de acesso a esta página.", i.e. no permission to access the page). + * + * A student's personal details. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetPersonalInfoPageStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getPersonalInfoPage({ + * faculty: "faculty", + * language: "language", + * pv_num_unico: "pv_num_unico", + * pv_dummy: "pv_dummy" + * }) + */ + public getPersonalInfoPage( + request: Sigarra.GetPersonalInfoPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getPersonalInfoPage(request, requestOptions)); + } + + private async __getPersonalInfoPage( + request: Sigarra.GetPersonalInfoPageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_num_unico: pvNumUnico, pv_dummy: pvDummy } = request; + const _queryParams: Record = { + pv_num_unico: pvNumUnico, + pv_dummy: pvDummy, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/fest_geral.info_pessoal_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/fest_geral.info_pessoal_view", + ); + } + + /** + * A student's schedule page. `#cal-shadow-container[data-evt-source-url]` has the URL for `calendar.getStudentEvents`. + * + * Used by [uni](https://github.com/NIAEFEUP/uni). + * + * @param {Sigarra.GetSchedulePageStudentRequest} request + * @param {StudentClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.student.getSchedulePage({ + * faculty: "faculty", + * language: "language", + * pv_num_unico: "pv_num_unico", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodos: "pv_periodos" + * }) + */ + public getSchedulePage( + request: Sigarra.GetSchedulePageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getSchedulePage(request, requestOptions)); + } + + private async __getSchedulePage( + request: Sigarra.GetSchedulePageStudentRequest, + requestOptions?: StudentClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_num_unico: pvNumUnico, + pv_ano_lectivo: pvAnoLectivo, + pv_periodos: pvPeriodos, + } = request; + const _queryParams: Record = { + pv_num_unico: pvNumUnico, + pv_ano_lectivo: pvAnoLectivo, + pv_periodos: pvPeriodos, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/hor_geral.estudantes_view`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + responseType: "text", + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as string, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/hor_geral.estudantes_view", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/index.ts b/packages/sigarra/src/generated/api/resources/student/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetAcademicPathStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetAcademicPathStudentRequest.ts new file mode 100644 index 000000000..bea825940 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetAcademicPathStudentRequest.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * } + */ +export interface GetAcademicPathStudentRequest { + faculty: string; + language: string; + /** Student number. */ + pv_codigo: string; + pv_ano_lectivo_min?: string; + pv_ano_lectivo_max?: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetCourseAcademicPathPageStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCourseAcademicPathPageStudentRequest.ts new file mode 100644 index 000000000..6f31c336e --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCourseAcademicPathPageStudentRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_fest_id: "pv_fest_id" + * } + */ +export interface GetCourseAcademicPathPageStudentRequest { + faculty: string; + language: string; + /** Course enrolment ID (`fest_id`). */ + pv_fest_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetCourseUnitEnrollmentsPageStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCourseUnitEnrollmentsPageStudentRequest.ts new file mode 100644 index 000000000..57a0419b1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCourseUnitEnrollmentsPageStudentRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_fest_id: "pv_fest_id" + * } + */ +export interface GetCourseUnitEnrollmentsPageStudentRequest { + faculty: string; + language: string; + /** Course enrolment ID (`fest_id`). */ + pv_fest_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetCoursesPageStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCoursesPageStudentRequest.ts new file mode 100644 index 000000000..76956cb1b --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCoursesPageStudentRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_num_unico: "pv_num_unico" + * } + */ +export interface GetCoursesPageStudentRequest { + faculty: string; + language: string; + /** Student number. */ + pv_num_unico: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetCurrentCoursesStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCurrentCoursesStudentRequest.ts new file mode 100644 index 000000000..b559bfad9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetCurrentCoursesStudentRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * } + */ +export interface GetCurrentCoursesStudentRequest { + faculty: string; + language: string; + /** Student number. */ + pv_codigo: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetExamsStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetExamsStudentRequest.ts new file mode 100644 index 000000000..0256a12e8 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetExamsStudentRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * } + */ +export interface GetExamsStudentRequest { + faculty: string; + language: string; + /** Student number. */ + pv_codigo: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetFacultiesPageStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetFacultiesPageStudentRequest.ts new file mode 100644 index 000000000..0ecbd7af1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetFacultiesPageStudentRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pct_codigo: "pct_codigo" + * } + */ +export interface GetFacultiesPageStudentRequest { + faculty: string; + language: string; + /** Username. */ + pct_codigo: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetPersonalInfoPageStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetPersonalInfoPageStudentRequest.ts new file mode 100644 index 000000000..805b51568 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetPersonalInfoPageStudentRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_num_unico: "pv_num_unico", + * pv_dummy: "pv_dummy" + * } + */ +export interface GetPersonalInfoPageStudentRequest { + faculty: string; + language: string; + /** Student number. */ + pv_num_unico: string; + /** Required, but any value works (even empty). `uni` sends the current timestamp. */ + pv_dummy: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetProfileStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetProfileStudentRequest.ts new file mode 100644 index 000000000..571860a55 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetProfileStudentRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo" + * } + */ +export interface GetProfileStudentRequest { + faculty: string; + language: string; + /** Student number. */ + pv_codigo: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetSchedulePageStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetSchedulePageStudentRequest.ts new file mode 100644 index 000000000..3c464f7e6 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetSchedulePageStudentRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "faculty", + * language: "language", + * pv_num_unico: "pv_num_unico", + * pv_ano_lectivo: "pv_ano_lectivo", + * pv_periodos: "pv_periodos" + * } + */ +export interface GetSchedulePageStudentRequest { + faculty: string; + language: string; + /** Student number. */ + pv_num_unico: string; + /** Academic year, e.g. `2023`. */ + pv_ano_lectivo: string; + /** `uni` sends `1`. */ + pv_periodos: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/GetScheduleStudentRequest.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/GetScheduleStudentRequest.ts new file mode 100644 index 000000000..04cd3e832 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/GetScheduleStudentRequest.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_codigo: "pv_codigo", + * pv_semana_ini: "pv_semana_ini", + * pv_semana_fim: "pv_semana_fim" + * } + */ +export interface GetScheduleStudentRequest { + faculty: string; + language: string; + pv_codigo: string; + /** Start of the period, as YYYYMMDD. */ + pv_semana_ini: string; + /** End of the period, as YYYYMMDD. */ + pv_semana_fim: string; +} diff --git a/packages/sigarra/src/generated/api/resources/student/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/student/client/requests/index.ts new file mode 100644 index 000000000..1589126aa --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/client/requests/index.ts @@ -0,0 +1,11 @@ +export type { GetAcademicPathStudentRequest } from "./GetAcademicPathStudentRequest.js"; +export type { GetCourseAcademicPathPageStudentRequest } from "./GetCourseAcademicPathPageStudentRequest.js"; +export type { GetCoursesPageStudentRequest } from "./GetCoursesPageStudentRequest.js"; +export type { GetCourseUnitEnrollmentsPageStudentRequest } from "./GetCourseUnitEnrollmentsPageStudentRequest.js"; +export type { GetCurrentCoursesStudentRequest } from "./GetCurrentCoursesStudentRequest.js"; +export type { GetExamsStudentRequest } from "./GetExamsStudentRequest.js"; +export type { GetFacultiesPageStudentRequest } from "./GetFacultiesPageStudentRequest.js"; +export type { GetPersonalInfoPageStudentRequest } from "./GetPersonalInfoPageStudentRequest.js"; +export type { GetProfileStudentRequest } from "./GetProfileStudentRequest.js"; +export type { GetSchedulePageStudentRequest } from "./GetSchedulePageStudentRequest.js"; +export type { GetScheduleStudentRequest } from "./GetScheduleStudentRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/student/exports.ts b/packages/sigarra/src/generated/api/resources/student/exports.ts new file mode 100644 index 000000000..f21de43eb --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { StudentClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/student/index.ts b/packages/sigarra/src/generated/api/resources/student/index.ts new file mode 100644 index 000000000..d9adb1af9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/index.ts @@ -0,0 +1,2 @@ +export * from "./client/index.js"; +export * from "./types/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/student/types/GetAcademicPathStudentResponseItem.ts b/packages/sigarra/src/generated/api/resources/student/types/GetAcademicPathStudentResponseItem.ts new file mode 100644 index 000000000..359a5648c --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/types/GetAcademicPathStudentResponseItem.ts @@ -0,0 +1,54 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetAcademicPathStudentResponseItem { + fest_id: number; + fest_tipo: string; + fest_tipo_descr: string; + cur_nome: string; + cur_name: string; + cur_sigla: string; + ano_curricular: string; + media: number; + inscricoes: GetAcademicPathStudentResponseItem.Inscricoes.Item[]; +} + +export namespace GetAcademicPathStudentResponseItem { + export type Inscricoes = Inscricoes.Item[]; + + export namespace Inscricoes { + export interface Item { + ucurr_id: number; + ucurr_codigo: string; + ucurr_sigla: string; + ucurr_nome: string; + ucurr_name: string; + a_lectivo: number; + ano: number; + ocorr_id: number; + ucurr_tipo: string; + ucurr_duracao_tipo: string; + id: number; + inst_id: number; + comp_id: number; + per_id: number; + per_codigo: string; + per_nome: string; + tipo: string; + estado: string; + frequencia: string; + avaliacao: string; + resultado_melhor: string; + resultado_ects: string; + aval_a_lectivo: number | null; + resultado_insc: string; + creditos_ucn: unknown | null; + creditos_ects: number; + n_ucurrs: number; + tipo_ucurrs: string; + obr_opt: string; + regra_eq: string; + devedor: string; + incumpridor: string; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/student/types/GetCurrentCoursesStudentResponseItem.ts b/packages/sigarra/src/generated/api/resources/student/types/GetCurrentCoursesStudentResponseItem.ts new file mode 100644 index 000000000..80f0d8d35 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/types/GetCurrentCoursesStudentResponseItem.ts @@ -0,0 +1,53 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetCurrentCoursesStudentResponseItem { + fest_id: number; + fest_tipo: string; + fest_tipo_descr: string; + cur_nome: string; + cur_name: string; + cur_sigla: string; + ano_curricular: string; + inscricoes: GetCurrentCoursesStudentResponseItem.Inscricoes.Item[]; +} + +export namespace GetCurrentCoursesStudentResponseItem { + export type Inscricoes = Inscricoes.Item[]; + + export namespace Inscricoes { + export interface Item { + ucurr_id: number; + ucurr_codigo: string; + ucurr_sigla: string; + ucurr_nome: string; + ucurr_name: string; + a_lectivo: number; + ano: number; + ocorr_id: number; + ucurr_tipo: string; + ucurr_duracao_tipo: string; + id: number; + inst_id: number; + comp_id: number; + per_id: number; + per_codigo: string; + per_nome: string; + tipo: string; + estado: string; + frequencia: string; + avaliacao: string; + resultado_melhor: string; + resultado_ects: string; + aval_a_lectivo: number | null; + resultado_insc: string; + creditos_ucn: unknown | null; + creditos_ects: number; + n_ucurrs: number; + tipo_ucurrs: string; + obr_opt: string; + regra_eq: string; + devedor: string; + incumpridor: string; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/student/types/GetExamsStudentResponseItem.ts b/packages/sigarra/src/generated/api/resources/student/types/GetExamsStudentResponseItem.ts new file mode 100644 index 000000000..1b05f6814 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/types/GetExamsStudentResponseItem.ts @@ -0,0 +1,36 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetExamsStudentResponseItem { + id?: number | undefined; + /** Occurrence ID. */ + ocorr_id?: number | undefined; + /** Occurrence name. */ + ocorr_nome?: string | undefined; + /** ISO 8601 */ + data?: string | undefined; + /** ISO 8601 */ + hora_inicio?: string | undefined; + /** ISO 8601 */ + hora_fim?: string | undefined; + duracao?: unknown | undefined; + /** Invigilator. */ + pfunc_id?: number | undefined; + /** Exam type. */ + tipo?: string | undefined; + /** Description of the exam type. */ + tipo_descricao?: string | undefined; + /** Name of the exam season. */ + epoca?: string | undefined; + salas?: GetExamsStudentResponseItem.Salas.Item[] | undefined; +} + +export namespace GetExamsStudentResponseItem { + export type Salas = Salas.Item[]; + + export namespace Salas { + export interface Item { + espaco_id?: number | undefined; + espaco_sigla?: string | undefined; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/student/types/GetProfileStudentResponse.ts b/packages/sigarra/src/generated/api/resources/student/types/GetProfileStudentResponse.ts new file mode 100644 index 000000000..f17d667d9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/types/GetProfileStudentResponse.ts @@ -0,0 +1,38 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetProfileStudentResponse { + codigo: string; + nome: string; + email: string; + email_alt: string; + pagina_web: string; + telemovel: string; + telefone: string; + cursos: GetProfileStudentResponse.Cursos.Item[]; +} + +export namespace GetProfileStudentResponse { + export type Cursos = Cursos.Item[]; + + export namespace Cursos { + export interface Item { + fest_id: number; + fest_tipo: string; + fest_tipo_descr: string; + cur_nome: string; + cur_name: string; + cur_sigla: string; + fest_a_lect_1_insc: number; + fest_d_1_insc: string; + cur_id: number; + est_alectivo: number; + est_sig: string; + est_nome: string; + est_d_inicio: string; + est_d_fim: string; + inst_nome: string; + inst_sigla: string; + ano_curricular: string; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/student/types/index.ts b/packages/sigarra/src/generated/api/resources/student/types/index.ts new file mode 100644 index 000000000..2481637b8 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/student/types/index.ts @@ -0,0 +1,4 @@ +export * from "./GetAcademicPathStudentResponseItem.js"; +export * from "./GetCurrentCoursesStudentResponseItem.js"; +export * from "./GetExamsStudentResponseItem.js"; +export * from "./GetProfileStudentResponse.js"; diff --git a/packages/sigarra/src/generated/api/resources/summaries/client/Client.ts b/packages/sigarra/src/generated/api/resources/summaries/client/Client.ts new file mode 100644 index 000000000..b3ce49f82 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/client/Client.ts @@ -0,0 +1,274 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptions, normalizeClientOptions } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as Sigarra from "../../../index.js"; + +export declare namespace SummariesClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +/** + * Class summaries: classes of a course unit, their lessons and lesson summaries. + */ +export class SummariesClient { + protected readonly _options: NormalizedClientOptions; + + constructor(options: SummariesClient.Options = {}) { + this._options = normalizeClientOptions(options); + } + + /** + * Lists the classes of a course unit occurrence. + * + * @param {Sigarra.GetClassesSummariesRequest} request + * @param {SummariesClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.summaries.getClasses({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484400" + * }) + */ + public getClasses( + request: Sigarra.GetClassesSummariesRequest, + requestOptions?: SummariesClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getClasses(request, requestOptions)); + } + + private async __getClasses( + request: Sigarra.GetClassesSummariesRequest, + requestOptions?: SummariesClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_ocorrencia_id: pvOcorrenciaId } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.sum_turmas`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.GetClassesSummariesResponse, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.sum_turmas", + ); + } + + /** + * Lists all the lessons of a class, with the ID of each lesson's summary. + * + * @param {Sigarra.GetLessonsSummariesRequest} request + * @param {SummariesClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.summaries.getLessons({ + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484400", + * pv_turma_id: "824506", + * pv_tipo_aula: "TP" + * }) + */ + public getLessons( + request: Sigarra.GetLessonsSummariesRequest, + requestOptions?: SummariesClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getLessons(request, requestOptions)); + } + + private async __getLessons( + request: Sigarra.GetLessonsSummariesRequest, + requestOptions?: SummariesClient.RequestOptions, + ): Promise> { + const { + faculty, + language, + pv_ocorrencia_id: pvOcorrenciaId, + pv_turma_id: pvTurmaId, + pv_tipo_aula: pvTipoAula, + } = request; + const _queryParams: Record = { + pv_ocorrencia_id: pvOcorrenciaId, + pv_turma_id: pvTurmaId, + pv_tipo_aula: pvTipoAula, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.sum_aulas`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as Sigarra.GetLessonsSummariesResponseItem[], + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.sum_aulas", + ); + } + + /** + * Gets the summary of a specific lesson. + * + * @param {Sigarra.GetSummarySummariesRequest} request + * @param {SummariesClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Sigarra.ForbiddenError} + * @throws {@link errors.SigarraError} + * @throws {@link errors.SigarraTimeoutError} + * + * @example + * await client.summaries.getSummary({ + * faculty: "feup", + * language: "pt", + * pv_sumario_id: "2146359" + * }) + */ + public getSummary( + request: Sigarra.GetSummarySummariesRequest, + requestOptions?: SummariesClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__getSummary(request, requestOptions)); + } + + private async __getSummary( + request: Sigarra.GetSummarySummariesRequest, + requestOptions?: SummariesClient.RequestOptions, + ): Promise> { + const { faculty, language, pv_sumario_id: pvSumarioId } = request; + const _queryParams: Record = { + pv_sumario_id: pvSumarioId, + }; + const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)) ?? + environments.SigarraEnvironment.Default, + `${core.url.encodePathParam(faculty)}/${core.url.encodePathParam(language)}/mob_ucurr_geral.sumario`, + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: _response.body as Sigarra.GetSummarySummariesResponse, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 403: + throw new Sigarra.ForbiddenError(_response.error.body as unknown, _response.rawResponse); + default: + throw new errors.SigarraError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/{faculty}/{language}/mob_ucurr_geral.sumario", + ); + } +} diff --git a/packages/sigarra/src/generated/api/resources/summaries/client/index.ts b/packages/sigarra/src/generated/api/resources/summaries/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetClassesSummariesRequest.ts b/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetClassesSummariesRequest.ts new file mode 100644 index 000000000..07aab3c20 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetClassesSummariesRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484400" + * } + */ +export interface GetClassesSummariesRequest { + faculty: string; + language: string; + /** Course unit occurrence ID. */ + pv_ocorrencia_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetLessonsSummariesRequest.ts b/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetLessonsSummariesRequest.ts new file mode 100644 index 000000000..3e4a26c23 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetLessonsSummariesRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_ocorrencia_id: "484400", + * pv_turma_id: "824506", + * pv_tipo_aula: "TP" + * } + */ +export interface GetLessonsSummariesRequest { + faculty: string; + language: string; + /** Course unit occurrence ID. */ + pv_ocorrencia_id: string; + /** Class ID. */ + pv_turma_id: string; + /** Lesson type. */ + pv_tipo_aula: string; +} diff --git a/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetSummarySummariesRequest.ts b/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetSummarySummariesRequest.ts new file mode 100644 index 000000000..bef2ba33f --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/client/requests/GetSummarySummariesRequest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * faculty: "feup", + * language: "pt", + * pv_sumario_id: "2146359" + * } + */ +export interface GetSummarySummariesRequest { + faculty: string; + language: string; + /** Summary ID. */ + pv_sumario_id: string; +} diff --git a/packages/sigarra/src/generated/api/resources/summaries/client/requests/index.ts b/packages/sigarra/src/generated/api/resources/summaries/client/requests/index.ts new file mode 100644 index 000000000..277478e32 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/client/requests/index.ts @@ -0,0 +1,3 @@ +export type { GetClassesSummariesRequest } from "./GetClassesSummariesRequest.js"; +export type { GetLessonsSummariesRequest } from "./GetLessonsSummariesRequest.js"; +export type { GetSummarySummariesRequest } from "./GetSummarySummariesRequest.js"; diff --git a/packages/sigarra/src/generated/api/resources/summaries/exports.ts b/packages/sigarra/src/generated/api/resources/summaries/exports.ts new file mode 100644 index 000000000..e22f8611e --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { SummariesClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/summaries/index.ts b/packages/sigarra/src/generated/api/resources/summaries/index.ts new file mode 100644 index 000000000..d9adb1af9 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/index.ts @@ -0,0 +1,2 @@ +export * from "./client/index.js"; +export * from "./types/index.js"; diff --git a/packages/sigarra/src/generated/api/resources/summaries/types/GetClassesSummariesResponse.ts b/packages/sigarra/src/generated/api/resources/summaries/types/GetClassesSummariesResponse.ts new file mode 100644 index 000000000..1927ec4c1 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/types/GetClassesSummariesResponse.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetClassesSummariesResponse { + turmas: GetClassesSummariesResponse.Turmas.Item[]; +} + +export namespace GetClassesSummariesResponse { + export type Turmas = Turmas.Item[]; + + export namespace Turmas { + export interface Item { + turma_id: number; + tipo: string; + sigla: string; + } + } +} diff --git a/packages/sigarra/src/generated/api/resources/summaries/types/GetLessonsSummariesResponseItem.ts b/packages/sigarra/src/generated/api/resources/summaries/types/GetLessonsSummariesResponseItem.ts new file mode 100644 index 000000000..cc1f124db --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/types/GetLessonsSummariesResponseItem.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetLessonsSummariesResponseItem { + n_aula: number; + cod_sumario: number; + data_prevista: string; + data_efectiva: string; +} diff --git a/packages/sigarra/src/generated/api/resources/summaries/types/GetSummarySummariesResponse.ts b/packages/sigarra/src/generated/api/resources/summaries/types/GetSummarySummariesResponse.ts new file mode 100644 index 000000000..f7726e125 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/types/GetSummarySummariesResponse.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface GetSummarySummariesResponse { + n_aula: number; + cod_sumario: number | null; + data_prevista: string; + data_efectiva: string; + obs: string; + sumario: string; + doc_codigo: string; + n_alunos: number | null; +} diff --git a/packages/sigarra/src/generated/api/resources/summaries/types/index.ts b/packages/sigarra/src/generated/api/resources/summaries/types/index.ts new file mode 100644 index 000000000..de1338b45 --- /dev/null +++ b/packages/sigarra/src/generated/api/resources/summaries/types/index.ts @@ -0,0 +1,3 @@ +export * from "./GetClassesSummariesResponse.js"; +export * from "./GetLessonsSummariesResponseItem.js"; +export * from "./GetSummarySummariesResponse.js"; diff --git a/packages/sigarra/src/generated/api/types/CalendarEvents.ts b/packages/sigarra/src/generated/api/types/CalendarEvents.ts new file mode 100644 index 000000000..7d10c709b --- /dev/null +++ b/packages/sigarra/src/generated/api/types/CalendarEvents.ts @@ -0,0 +1,142 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface CalendarEvents { + data: CalendarEvents.Data.Item[]; + meta: CalendarEvents.Meta; +} + +export namespace CalendarEvents { + export type Data = Data.Item[]; + + export namespace Data { + export interface Item { + start: string; + end: string; + id: number; + name: string; + state: Item.State; + is_inst_req: boolean; + hour_start: string; + hour_end: string; + first_date: string; + last_date: string; + week_days: string[]; + week_count: number; + ucs: Item.Ucs.Item[]; + classes: Item.Classes.Item[]; + persons: Item.Persons.Item[]; + rooms: Item.Rooms.Item[]; + typology: Item.Typology; + css_class: string[]; + } + + export namespace Item { + export interface State { + code: string; + name: string; + } + + export type Ucs = Ucs.Item[]; + + export namespace Ucs { + export interface Item { + id: number; + acronym: string; + name: string; + sigarra_id: number; + sigarra_url: string; + } + } + + export type Classes = Classes.Item[]; + + export namespace Classes { + export interface Item { + id: number; + acronym: string; + name: string; + sigarra_id: number; + sigarra_url: string; + } + } + + export type Persons = Persons.Item[]; + + export namespace Persons { + export interface Item { + id: number; + acronym: string; + name: string; + sigarra_id: number | null; + sigarra_url: string | null; + } + } + + export type Rooms = Rooms.Item[]; + + export namespace Rooms { + export interface Item { + id: number; + acronym: string; + name: string; + sigarra_id: number; + sigarra_url: string; + } + } + + export interface Typology { + acronym: string; + name: string; + } + } + } + + export interface Meta { + legend: Meta.Legend.Item[]; + cal_type: string; + cal_params: Meta.CalParams; + date_range: Meta.DateRange; + count: number; + } + + export namespace Meta { + export type Legend = Legend.Item[]; + + export namespace Legend { + export interface Item { + name: string; + acronym: string; + color_class: string; + } + } + + export interface CalParams { + locale: string; + academic_year_range: CalParams.AcademicYearRange; + day_time_range: CalParams.DayTimeRange; + lunch_time_range: CalParams.LunchTimeRange; + } + + export namespace CalParams { + export interface AcademicYearRange { + start: string; + end: string; + } + + export interface DayTimeRange { + start: string; + end: string; + } + + export interface LunchTimeRange { + start: string; + end: string; + } + } + + export interface DateRange { + start: string; + end: string; + } + } +} diff --git a/packages/sigarra/src/generated/api/types/Error_.ts b/packages/sigarra/src/generated/api/types/Error_.ts new file mode 100644 index 000000000..74097fe75 --- /dev/null +++ b/packages/sigarra/src/generated/api/types/Error_.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface Error_ { + /** Error type, e.g. `Autorização`. */ + erro: string; + /** Error message. */ + erro_msg: string; +} diff --git a/packages/sigarra/src/generated/api/types/LoginResult.ts b/packages/sigarra/src/generated/api/types/LoginResult.ts new file mode 100644 index 000000000..1244e1080 --- /dev/null +++ b/packages/sigarra/src/generated/api/types/LoginResult.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface LoginResult { + authenticated: boolean; + /** Username, without the `up` prefix (e.g. `20YYXXXXX`). */ + codigo: string; + /** User type: `A` for students. Values for other kinds of users haven't been observed. */ + tipo: string; +} diff --git a/packages/sigarra/src/generated/api/types/WeeklySchedule.ts b/packages/sigarra/src/generated/api/types/WeeklySchedule.ts new file mode 100644 index 000000000..8337bc51f --- /dev/null +++ b/packages/sigarra/src/generated/api/types/WeeklySchedule.ts @@ -0,0 +1,72 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface WeeklySchedule { + horario: WeeklySchedule.Horario.Item[]; + blocos?: WeeklySchedule.Blocos.Item[] | undefined; +} + +export namespace WeeklySchedule { + export type Horario = Horario.Item[]; + + export namespace Horario { + export interface Item { + dia: number; + hora_inicio: number; + ucurr_sigla: string; + ocorrencia_id: number; + tipo: string; + aula_id: number; + aula_duracao: number; + sala_sigla: string; + salas: Item.Salas.Item[]; + doc_sigla: string; + docentes: Item.Docentes.Item[]; + turma_sigla: string; + turmas: Item.Turmas.Item[]; + periodo: number; + } + + export namespace Item { + export type Salas = Salas.Item[]; + + export namespace Salas { + export interface Item { + espaco_id: number; + espaco_nome: string; + } + } + + export type Docentes = Docentes.Item[]; + + export namespace Docentes { + export interface Item { + doc_codigo: string; + doc_nome: string; + } + } + + export type Turmas = Turmas.Item[]; + + export namespace Turmas { + export interface Item { + turma_id: number; + turma_sigla: string; + } + } + } + } + + export type Blocos = Blocos.Item[]; + + export namespace Blocos { + export interface Item { + semana_ini: string; + semana_fim: string; + bloco_codigo: unknown | null; + c_turma_id: unknown | null; + turmas_bloco: string; + turma_real: string; + periodo: string; + } + } +} diff --git a/packages/sigarra/src/generated/api/types/index.ts b/packages/sigarra/src/generated/api/types/index.ts new file mode 100644 index 000000000..c6f0689b5 --- /dev/null +++ b/packages/sigarra/src/generated/api/types/index.ts @@ -0,0 +1,4 @@ +export * from "./CalendarEvents.js"; +export * from "./Error_.js"; +export * from "./LoginResult.js"; +export * from "./WeeklySchedule.js"; diff --git a/packages/sigarra/src/generated/core/exports.ts b/packages/sigarra/src/generated/core/exports.ts new file mode 100644 index 000000000..69296d710 --- /dev/null +++ b/packages/sigarra/src/generated/core/exports.ts @@ -0,0 +1 @@ +export * from "./logging/exports.js"; diff --git a/packages/sigarra/src/generated/core/fetcher/APIResponse.ts b/packages/sigarra/src/generated/core/fetcher/APIResponse.ts new file mode 100644 index 000000000..97ab83c2b --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/APIResponse.ts @@ -0,0 +1,23 @@ +import type { RawResponse } from "./RawResponse.js"; + +/** + * The response of an API call. + * It is a successful response or a failed response. + */ +export type APIResponse = SuccessfulResponse | FailedResponse; + +export interface SuccessfulResponse { + ok: true; + body: T; + /** + * @deprecated Use `rawResponse` instead + */ + headers?: Record; + rawResponse: RawResponse; +} + +export interface FailedResponse { + ok: false; + error: T; + rawResponse: RawResponse; +} diff --git a/packages/sigarra/src/generated/core/fetcher/BinaryResponse.ts b/packages/sigarra/src/generated/core/fetcher/BinaryResponse.ts new file mode 100644 index 000000000..b9e40fb62 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/BinaryResponse.ts @@ -0,0 +1,34 @@ +export type BinaryResponse = { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ + bodyUsed: Response["bodyUsed"]; + /** + * Returns a ReadableStream of the response body. + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) + */ + stream: () => Response["body"]; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ + arrayBuffer: () => ReturnType; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ + blob: () => ReturnType; + /** + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) + * Some versions of the Fetch API may not support this method. + */ + bytes?(): Promise; +}; + +export function getBinaryResponse(response: Response): BinaryResponse { + const binaryResponse: BinaryResponse = { + get bodyUsed() { + return response.bodyUsed; + }, + stream: () => response.body, + arrayBuffer: response.arrayBuffer.bind(response), + blob: response.blob.bind(response), + }; + if ("bytes" in response && typeof response.bytes === "function") { + binaryResponse.bytes = response.bytes.bind(response); + } + + return binaryResponse; +} diff --git a/packages/sigarra/src/generated/core/fetcher/EndpointMetadata.ts b/packages/sigarra/src/generated/core/fetcher/EndpointMetadata.ts new file mode 100644 index 000000000..998d68f5c --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/EndpointMetadata.ts @@ -0,0 +1,13 @@ +export type SecuritySchemeKey = string; +/** + * A collection of security schemes, where the key is the name of the security scheme and the value is the list of scopes required for that scheme. + * All schemes in the collection must be satisfied for authentication to be successful. + */ +export type SecuritySchemeCollection = Record; +export type AuthScope = string; +export type EndpointMetadata = { + /** + * An array of security scheme collections. Each collection represents an alternative way to authenticate. + */ + security?: SecuritySchemeCollection[]; +}; diff --git a/packages/sigarra/src/generated/core/fetcher/EndpointSupplier.ts b/packages/sigarra/src/generated/core/fetcher/EndpointSupplier.ts new file mode 100644 index 000000000..aad81f0d9 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/EndpointSupplier.ts @@ -0,0 +1,14 @@ +import type { EndpointMetadata } from "./EndpointMetadata.js"; +import type { Supplier } from "./Supplier.js"; + +type EndpointSupplierFn = (arg: { endpointMetadata?: EndpointMetadata }) => T | Promise; +export type EndpointSupplier = Supplier | EndpointSupplierFn; +export const EndpointSupplier = { + get: async (supplier: EndpointSupplier, arg: { endpointMetadata?: EndpointMetadata }): Promise => { + if (typeof supplier === "function") { + return (supplier as EndpointSupplierFn)(arg); + } else { + return supplier; + } + }, +}; diff --git a/packages/sigarra/src/generated/core/fetcher/Fetcher.ts b/packages/sigarra/src/generated/core/fetcher/Fetcher.ts new file mode 100644 index 000000000..cd5c5793d --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/Fetcher.ts @@ -0,0 +1,311 @@ +import { toJson } from "../json.js"; +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import type { APIResponse } from "./APIResponse.js"; +import { createRequestUrl } from "./createRequestUrl.js"; +import type { EndpointMetadata } from "./EndpointMetadata.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getErrorResponseBody } from "./getErrorResponseBody.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { getRequestBody } from "./getRequestBody.js"; +import { getResponseBody } from "./getResponseBody.js"; +import { Headers } from "./Headers.js"; +import { makeRequest } from "./makeRequest.js"; +import { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; +import { redactUrl, SENSITIVE_QUERY_PARAMS } from "./redactUrl.js"; +import { requestWithRetries } from "./requestWithRetries.js"; + +export type FetchFunction = (args: Fetcher.Args) => Promise>; + +export declare namespace Fetcher { + export interface Args { + url: string; + method: string; + contentType?: string; + headers?: Record; + /** + * @deprecated Prefer `queryString` (produced by `core.url.queryBuilder()`). + * Retained for backwards compatibility with custom fetchers and callers that + * still construct request args with a query-parameter object. + */ + queryParameters?: Record; + queryString?: string; + body?: unknown; + timeoutMs?: number; + maxRetries?: number; + withCredentials?: boolean; + abortSignal?: AbortSignal; + requestType?: "json" | "file" | "bytes" | "form" | "other"; + responseType?: "json" | "blob" | "sse" | "streaming" | "text" | "arrayBuffer" | "binary-response"; + duplex?: "half"; + endpointMetadata?: EndpointMetadata; + fetchFn?: typeof fetch; + logging?: LogConfig | Logger; + } + + export type Error = FailedStatusCodeError | NonJsonError | BodyIsNullError | TimeoutError | UnknownError; + + export interface FailedStatusCodeError { + reason: "status-code"; + statusCode: number; + body: unknown; + } + + export interface NonJsonError { + reason: "non-json"; + statusCode: number; + rawBody: string; + } + + export interface BodyIsNullError { + reason: "body-is-null"; + statusCode: number; + } + + export interface TimeoutError { + reason: "timeout"; + cause?: unknown; + } + + export interface UnknownError { + reason: "unknown"; + errorMessage: string; + cause?: unknown; + } +} + +const SENSITIVE_HEADERS = new Set([ + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", +]); + +function redactHeaders(headers: Headers | Record): Record { + const filtered: Record = {}; + for (const [key, value] of headers instanceof Headers ? headers.entries() : Object.entries(headers)) { + if (SENSITIVE_HEADERS.has(key.toLowerCase())) { + filtered[key] = "[REDACTED]"; + } else { + filtered[key] = value; + } + } + return filtered; +} + +function redactQueryParameters( + queryParameters: Record | undefined, +): Record | undefined { + if (queryParameters == null) { + return undefined; + } + const redacted: Record = {}; + for (const [key, value] of Object.entries(queryParameters)) { + redacted[key] = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()) ? "[REDACTED]" : value; + } + return redacted; +} + +async function getHeaders(args: Fetcher.Args): Promise { + const newHeaders: Headers = new Headers(); + + newHeaders.set( + "Accept", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", + ); + if (args.body !== undefined && args.contentType != null) { + newHeaders.set("Content-Type", args.contentType); + } + + if (args.headers == null) { + return newHeaders; + } + + for (const [key, value] of Object.entries(args.headers)) { + const result = await EndpointSupplier.get(value, { endpointMetadata: args.endpointMetadata ?? {} }); + if (typeof result === "string") { + newHeaders.set(key, result); + continue; + } + if (result == null) { + continue; + } + newHeaders.set(key, `${result}`); + } + return newHeaders; +} + +export async function fetcherImpl(args: Fetcher.Args): Promise> { + let url = args.url; + if (args.queryString != null && args.queryString.length > 0) { + url = `${url}?${args.queryString}`; + } else { + url = createRequestUrl(args.url, args.queryParameters); + } + const requestBody: BodyInit | undefined = await getRequestBody({ + body: args.body, + type: args.requestType ?? "other", + }); + const fetchFn = args.fetchFn ?? (await getFetchFn()); + const headers = await getHeaders(args); + const logger = createLogger(args.logging); + + if (logger.isDebug()) { + const metadata = { + method: args.method, + url: redactUrl(url), + headers: redactHeaders(headers), + queryParameters: redactQueryParameters(args.queryParameters), + hasBody: requestBody != null, + }; + logger.debug("Making HTTP request", metadata); + } + + try { + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + url, + args.method, + headers, + requestBody, + args.timeoutMs, + args.abortSignal, + args.withCredentials, + args.duplex, + args.responseType === "streaming" || args.responseType === "sse", + ), + args.maxRetries, + ); + + if (response.status >= 200 && response.status < 400) { + if (logger.isDebug()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + responseHeaders: redactHeaders(response.headers), + }; + logger.debug("HTTP request succeeded", metadata); + } + const body = await getResponseBody(response, args.responseType); + return { + ok: true, + body: body as R, + headers: response.headers, + rawResponse: toRawResponse(response), + }; + } else { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + responseHeaders: redactHeaders(Object.fromEntries(response.headers.entries())), + }; + logger.error("HTTP request failed with error status", metadata); + } + return { + ok: false, + error: { + reason: "status-code", + statusCode: response.status, + body: await getErrorResponseBody(response), + }, + rawResponse: toRawResponse(response), + }; + } + } catch (error) { + if (args.abortSignal?.aborted) { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + }; + logger.error("HTTP request was aborted", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: "The user aborted a request", + cause: error, + }, + rawResponse: abortRawResponse, + }; + } else if (error instanceof Error && error.name === "AbortError") { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + timeoutMs: args.timeoutMs, + }; + logger.error("HTTP request timed out", metadata); + } + return { + ok: false, + error: { + reason: "timeout", + cause: error, + }, + rawResponse: abortRawResponse, + }; + } else if (error instanceof Error) { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + errorMessage: error.message, + }; + logger.error("HTTP request failed with error", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: error.message, + cause: error, + }, + rawResponse: unknownRawResponse, + }; + } + + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + error: toJson(error), + }; + logger.error("HTTP request failed with unknown error", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: toJson(error), + cause: error, + }, + rawResponse: unknownRawResponse, + }; + } +} + +export const fetcher: FetchFunction = fetcherImpl; diff --git a/packages/sigarra/src/generated/core/fetcher/Headers.ts b/packages/sigarra/src/generated/core/fetcher/Headers.ts new file mode 100644 index 000000000..af841aa24 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/Headers.ts @@ -0,0 +1,93 @@ +let Headers: typeof globalThis.Headers; + +if (typeof globalThis.Headers !== "undefined") { + Headers = globalThis.Headers; +} else { + Headers = class Headers implements Headers { + private headers: Map; + + constructor(init?: HeadersInit) { + this.headers = new Map(); + + if (init) { + if (init instanceof Headers) { + init.forEach((value, key) => this.append(key, value)); + } else if (Array.isArray(init)) { + for (const [key, value] of init) { + if (typeof key === "string" && typeof value === "string") { + this.append(key, value); + } else { + throw new TypeError("Each header entry must be a [string, string] tuple"); + } + } + } else { + for (const [key, value] of Object.entries(init)) { + if (typeof value === "string") { + this.append(key, value); + } else { + throw new TypeError("Header values must be strings"); + } + } + } + } + } + + append(name: string, value: string): void { + const key = name.toLowerCase(); + const existing = this.headers.get(key) || []; + this.headers.set(key, [...existing, value]); + } + + delete(name: string): void { + const key = name.toLowerCase(); + this.headers.delete(key); + } + + get(name: string): string | null { + const key = name.toLowerCase(); + const values = this.headers.get(key); + return values ? values.join(", ") : null; + } + + has(name: string): boolean { + const key = name.toLowerCase(); + return this.headers.has(key); + } + + set(name: string, value: string): void { + const key = name.toLowerCase(); + this.headers.set(key, [value]); + } + + forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: unknown): void { + const boundCallback = thisArg ? callbackfn.bind(thisArg) : callbackfn; + this.headers.forEach((values, key) => boundCallback(values.join(", "), key, this)); + } + + getSetCookie(): string[] { + return this.headers.get("set-cookie") || []; + } + + *entries(): HeadersIterator<[string, string]> { + for (const [key, values] of this.headers.entries()) { + yield [key, values.join(", ")]; + } + } + + *keys(): HeadersIterator { + yield* this.headers.keys(); + } + + *values(): HeadersIterator { + for (const values of this.headers.values()) { + yield values.join(", "); + } + } + + [Symbol.iterator](): HeadersIterator<[string, string]> { + return this.entries(); + } + }; +} + +export { Headers }; diff --git a/packages/sigarra/src/generated/core/fetcher/HttpResponsePromise.ts b/packages/sigarra/src/generated/core/fetcher/HttpResponsePromise.ts new file mode 100644 index 000000000..692ca7d79 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/HttpResponsePromise.ts @@ -0,0 +1,116 @@ +import type { WithRawResponse } from "./RawResponse.js"; + +/** + * A promise that returns the parsed response and lets you retrieve the raw response too. + */ +export class HttpResponsePromise extends Promise { + private innerPromise: Promise>; + private unwrappedPromise: Promise | undefined; + + private constructor(promise: Promise>) { + // Initialize with a no-op to avoid premature parsing + super((resolve) => { + resolve(undefined as unknown as T); + }); + this.innerPromise = promise; + } + + /** + * Creates an `HttpResponsePromise` from a function that returns a promise. + * + * @param fn - A function that returns a promise resolving to a `WithRawResponse` object. + * @param args - Arguments to pass to the function. + * @returns An `HttpResponsePromise` instance. + */ + public static fromFunction Promise>, T>( + fn: F, + ...args: Parameters + ): HttpResponsePromise { + return new HttpResponsePromise(fn(...args)); + } + + /** + * Creates a function that returns an `HttpResponsePromise` from a function that returns a promise. + * + * @param fn - A function that returns a promise resolving to a `WithRawResponse` object. + * @returns A function that returns an `HttpResponsePromise` instance. + */ + public static interceptFunction< + F extends (...args: never[]) => Promise>, + T = Awaited>["data"], + >(fn: F): (...args: Parameters) => HttpResponsePromise { + return (...args: Parameters): HttpResponsePromise => { + return HttpResponsePromise.fromPromise(fn(...args)); + }; + } + + /** + * Creates an `HttpResponsePromise` from an existing promise. + * + * @param promise - A promise resolving to a `WithRawResponse` object. + * @returns An `HttpResponsePromise` instance. + */ + public static fromPromise(promise: Promise>): HttpResponsePromise { + return new HttpResponsePromise(promise); + } + + /** + * Creates an `HttpResponsePromise` from an executor function. + * + * @param executor - A function that takes resolve and reject callbacks to create a promise. + * @returns An `HttpResponsePromise` instance. + */ + public static fromExecutor( + executor: (resolve: (value: WithRawResponse) => void, reject: (reason?: unknown) => void) => void, + ): HttpResponsePromise { + const promise = new Promise>(executor); + return new HttpResponsePromise(promise); + } + + /** + * Creates an `HttpResponsePromise` from a resolved result. + * + * @param result - A `WithRawResponse` object to resolve immediately. + * @returns An `HttpResponsePromise` instance. + */ + public static fromResult(result: WithRawResponse): HttpResponsePromise { + const promise = Promise.resolve(result); + return new HttpResponsePromise(promise); + } + + private unwrap(): Promise { + if (!this.unwrappedPromise) { + this.unwrappedPromise = this.innerPromise.then(({ data }) => data); + } + return this.unwrappedPromise; + } + + /** @inheritdoc */ + public override then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ): Promise { + return this.unwrap().then(onfulfilled, onrejected); + } + + /** @inheritdoc */ + public override catch( + onrejected?: ((reason: unknown) => TResult | PromiseLike) | null, + ): Promise { + return this.unwrap().catch(onrejected); + } + + /** @inheritdoc */ + public override finally(onfinally?: (() => void) | null): Promise { + return this.unwrap().finally(onfinally); + } + + /** + * Retrieves the data and raw response. + * + * @returns A promise resolving to a `WithRawResponse` object. + */ + public async withRawResponse(): Promise> { + return await this.innerPromise; + } +} diff --git a/packages/sigarra/src/generated/core/fetcher/RawResponse.ts b/packages/sigarra/src/generated/core/fetcher/RawResponse.ts new file mode 100644 index 000000000..37fb44e2a --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/RawResponse.ts @@ -0,0 +1,61 @@ +import { Headers } from "./Headers.js"; + +/** + * The raw response from the fetch call excluding the body. + */ +export type RawResponse = Omit< + { + [K in keyof Response as Response[K] extends Function ? never : K]: Response[K]; // strips out functions + }, + "ok" | "body" | "bodyUsed" +>; // strips out body and bodyUsed + +/** + * A raw response indicating that the request was aborted. + */ +export const abortRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 499, + statusText: "Client Closed Request", + type: "error", + url: "", +} as const; + +/** + * A raw response indicating an unknown error. + */ +export const unknownRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 0, + statusText: "Unknown Error", + type: "error", + url: "", +} as const; + +/** + * Converts a `RawResponse` object into a `RawResponse` by extracting its properties, + * excluding the `body` and `bodyUsed` fields. + * + * @param response - The `RawResponse` object to convert. + * @returns A `RawResponse` object containing the extracted properties of the input response. + */ +export function toRawResponse(response: Response): RawResponse { + return { + headers: response.headers, + redirected: response.redirected, + status: response.status, + statusText: response.statusText, + type: response.type, + url: response.url, + }; +} + +/** + * Creates a `RawResponse` from a standard `Response` object. + */ +export interface WithRawResponse { + readonly data: T; + readonly rawResponse: RawResponse; +} diff --git a/packages/sigarra/src/generated/core/fetcher/Supplier.ts b/packages/sigarra/src/generated/core/fetcher/Supplier.ts new file mode 100644 index 000000000..867c931c0 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/Supplier.ts @@ -0,0 +1,11 @@ +export type Supplier = T | Promise | (() => T | Promise); + +export const Supplier = { + get: async (supplier: Supplier): Promise => { + if (typeof supplier === "function") { + return (supplier as () => T)(); + } else { + return supplier; + } + }, +}; diff --git a/packages/sigarra/src/generated/core/fetcher/createRequestUrl.ts b/packages/sigarra/src/generated/core/fetcher/createRequestUrl.ts new file mode 100644 index 000000000..88e13265e --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/createRequestUrl.ts @@ -0,0 +1,6 @@ +import { toQueryString } from "../url/qs.js"; + +export function createRequestUrl(baseUrl: string, queryParameters?: Record): string { + const queryString = toQueryString(queryParameters, { arrayFormat: "repeat" }); + return queryString ? `${baseUrl}?${queryString}` : baseUrl; +} diff --git a/packages/sigarra/src/generated/core/fetcher/getErrorResponseBody.ts b/packages/sigarra/src/generated/core/fetcher/getErrorResponseBody.ts new file mode 100644 index 000000000..7cf4e623c --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/getErrorResponseBody.ts @@ -0,0 +1,33 @@ +import { fromJson } from "../json.js"; +import { getResponseBody } from "./getResponseBody.js"; + +export async function getErrorResponseBody(response: Response): Promise { + let contentType = response.headers.get("Content-Type")?.toLowerCase(); + if (contentType == null || contentType.length === 0) { + return getResponseBody(response); + } + + if (contentType.indexOf(";") !== -1) { + contentType = contentType.split(";")[0]?.trim() ?? ""; + } + switch (contentType) { + case "application/hal+json": + case "application/json": + case "application/ld+json": + case "application/problem+json": + case "application/vnd.api+json": + case "text/json": { + const text = await response.text(); + return text.length > 0 ? fromJson(text) : undefined; + } + default: + if (contentType.startsWith("application/vnd.") && contentType.endsWith("+json")) { + const text = await response.text(); + return text.length > 0 ? fromJson(text) : undefined; + } + + // Fallback to plain text if content type is not recognized + // Even if no body is present, the response will be an empty string + return await response.text(); + } +} diff --git a/packages/sigarra/src/generated/core/fetcher/getFetchFn.ts b/packages/sigarra/src/generated/core/fetcher/getFetchFn.ts new file mode 100644 index 000000000..9f845b956 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/getFetchFn.ts @@ -0,0 +1,3 @@ +export async function getFetchFn(): Promise { + return fetch; +} diff --git a/packages/sigarra/src/generated/core/fetcher/getHeader.ts b/packages/sigarra/src/generated/core/fetcher/getHeader.ts new file mode 100644 index 000000000..50f922b0e --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/getHeader.ts @@ -0,0 +1,8 @@ +export function getHeader(headers: Record, header: string): string | undefined { + for (const [headerKey, headerValue] of Object.entries(headers)) { + if (headerKey.toLowerCase() === header.toLowerCase()) { + return headerValue; + } + } + return undefined; +} diff --git a/packages/sigarra/src/generated/core/fetcher/getRequestBody.ts b/packages/sigarra/src/generated/core/fetcher/getRequestBody.ts new file mode 100644 index 000000000..91d9d81f5 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/getRequestBody.ts @@ -0,0 +1,20 @@ +import { toJson } from "../json.js"; +import { toQueryString } from "../url/qs.js"; + +export declare namespace GetRequestBody { + interface Args { + body: unknown; + type: "json" | "file" | "bytes" | "form" | "other"; + } +} + +export async function getRequestBody({ body, type }: GetRequestBody.Args): Promise { + if (type === "form") { + return toQueryString(body, { arrayFormat: "repeat", encode: true }); + } + if (type.includes("json")) { + return toJson(body); + } else { + return body as BodyInit; + } +} diff --git a/packages/sigarra/src/generated/core/fetcher/getResponseBody.ts b/packages/sigarra/src/generated/core/fetcher/getResponseBody.ts new file mode 100644 index 000000000..2e831e495 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/getResponseBody.ts @@ -0,0 +1,70 @@ +import { fromJson } from "../json.js"; +import { getBinaryResponse } from "./BinaryResponse.js"; + +// Pins the upstream Response so undici's FinalizationRegistry can't GC it and cancel the body stream. +function retainResponse(target: object, response: Response): void { + Object.defineProperty(target, "__fern_response_ref", { + value: response, + enumerable: false, + configurable: true, + writable: false, + }); +} + +export async function getResponseBody(response: Response, responseType?: string): Promise { + switch (responseType) { + case "binary-response": + return getBinaryResponse(response); + case "blob": + return await response.blob(); + case "arrayBuffer": + return await response.arrayBuffer(); + case "sse": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } + retainResponse(response.body, response); + return response.body; + case "streaming": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } + + retainResponse(response.body, response); + return response.body; + + case "text": + return await response.text(); + } + + // if responseType is "json" or not specified, try to parse as JSON + const text = await response.text(); + if (text.length > 0) { + try { + const responseBody = fromJson(text); + return responseBody; + } catch (_err) { + return { + ok: false, + error: { + reason: "non-json", + statusCode: response.status, + rawBody: text, + }, + }; + } + } + return undefined; +} diff --git a/packages/sigarra/src/generated/core/fetcher/index.ts b/packages/sigarra/src/generated/core/fetcher/index.ts new file mode 100644 index 000000000..bd5db362c --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/index.ts @@ -0,0 +1,13 @@ +export type { APIResponse } from "./APIResponse.js"; +export type { BinaryResponse } from "./BinaryResponse.js"; +export type { EndpointMetadata } from "./EndpointMetadata.js"; +export { EndpointSupplier } from "./EndpointSupplier.js"; +export type { Fetcher, FetchFunction } from "./Fetcher.js"; +export { fetcher } from "./Fetcher.js"; +export { getHeader } from "./getHeader.js"; +export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; +export type { RawResponse, WithRawResponse } from "./RawResponse.js"; +export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; +export { Supplier } from "./Supplier.js"; diff --git a/packages/sigarra/src/generated/core/fetcher/makePassthroughRequest.ts b/packages/sigarra/src/generated/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000..86cda011c --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,211 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { redactUrl } from "./redactUrl.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 0. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers, but only when the resolved URL targets the configured base URL. + // This prevents the SDK's credentials from leaking to an unrelated host when a caller + // passes an absolute cross-origin URL into the passthrough fetch escape hatch. + if (clientOptions.getAuthHeaders != null && targetsBaseUrl(fullUrl, baseUrl)) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: redactUrl(fullUrl), + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: redactUrl(fullUrl), + statusCode: response.status, + }); + } + + return response; +} + +/** + * Returns true when the resolved request URL points at the same origin as the + * configured base URL. Relative paths are always joined onto the base URL, so + * they resolve to the base origin and return true. Absolute URLs only match when + * their origin equals the base origin. When there is no base URL to compare + * against, or either value is not a parseable absolute URL, this returns false so + * auth headers are not attached. + */ +function targetsBaseUrl(fullUrl: string, baseUrl: string | undefined): boolean { + if (baseUrl == null) { + return false; + } + try { + return new URL(fullUrl).origin === new URL(baseUrl).origin; + } catch { + return false; + } +} diff --git a/packages/sigarra/src/generated/core/fetcher/makeRequest.ts b/packages/sigarra/src/generated/core/fetcher/makeRequest.ts new file mode 100644 index 000000000..360a86df4 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/makeRequest.ts @@ -0,0 +1,70 @@ +import { anySignal, getTimeoutSignal } from "./signals.js"; + +/** + * Cached result of checking whether the current runtime supports + * the `cache` option in `Request`. Some runtimes (e.g. Cloudflare Workers) + * throw a TypeError when this option is used. + */ +let _cacheNoStoreSupported: boolean | undefined; +export function isCacheNoStoreSupported(): boolean { + if (_cacheNoStoreSupported != null) { + return _cacheNoStoreSupported; + } + try { + new Request("http://localhost", { cache: "no-store" }); + _cacheNoStoreSupported = true; + } catch { + _cacheNoStoreSupported = false; + } + return _cacheNoStoreSupported; +} + +/** + * Reset the cached result of `isCacheNoStoreSupported`. Exposed for testing only. + */ +export function resetCacheNoStoreSupported(): void { + _cacheNoStoreSupported = undefined; +} + +export const makeRequest = async ( + fetchFn: (url: string, init: RequestInit) => Promise, + url: string, + method: string, + headers: Headers | Record, + requestBody: BodyInit | undefined, + timeoutMs?: number, + abortSignal?: AbortSignal, + withCredentials?: boolean, + duplex?: "half", + disableCache?: boolean, +): Promise => { + const signals: AbortSignal[] = []; + + let timeoutAbortId: ReturnType | undefined; + if (timeoutMs != null) { + const { signal, abortId } = getTimeoutSignal(timeoutMs); + timeoutAbortId = abortId; + signals.push(signal); + } + + if (abortSignal != null) { + signals.push(abortSignal); + } + const newSignals = anySignal(signals); + const response = await fetchFn(url, { + method: method, + headers, + body: requestBody, + signal: newSignals, + credentials: withCredentials ? "include" : undefined, + // @ts-ignore + duplex, + ...(disableCache && isCacheNoStoreSupported() ? { cache: "no-store" as RequestCache } : {}), + }); + + if (timeoutAbortId != null) { + clearTimeout(timeoutAbortId); + } + + return response; +}; diff --git a/packages/sigarra/src/generated/core/fetcher/redactUrl.ts b/packages/sigarra/src/generated/core/fetcher/redactUrl.ts new file mode 100644 index 000000000..3c2e897a6 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/redactUrl.ts @@ -0,0 +1,102 @@ +export const SENSITIVE_QUERY_PARAMS: Set = new Set([ + "api_key", + "api-key", + "apikey", + "token", + "access_token", + "access-token", + "auth_token", + "auth-token", + "password", + "passwd", + "secret", + "api_secret", + "api-secret", + "apisecret", + "key", + "session", + "session_id", + "session-id", +]); + +export function redactUrl(url: string): string { + const protocolIndex = url.indexOf("://"); + if (protocolIndex === -1) return url; + + const afterProtocol = protocolIndex + 3; + + // Find the first delimiter that marks the end of the authority section + const pathStart = url.indexOf("/", afterProtocol); + let queryStart = url.indexOf("?", afterProtocol); + let fragmentStart = url.indexOf("#", afterProtocol); + + const firstDelimiter = Math.min( + pathStart === -1 ? url.length : pathStart, + queryStart === -1 ? url.length : queryStart, + fragmentStart === -1 ? url.length : fragmentStart, + ); + + // Find the LAST @ before the delimiter (handles multiple @ in credentials) + let atIndex = -1; + for (let i = afterProtocol; i < firstDelimiter; i++) { + if (url[i] === "@") { + atIndex = i; + } + } + + if (atIndex !== -1) { + url = `${url.slice(0, afterProtocol)}[REDACTED]@${url.slice(atIndex + 1)}`; + } + + // Recalculate queryStart since url might have changed + queryStart = url.indexOf("?"); + if (queryStart === -1) return url; + + fragmentStart = url.indexOf("#", queryStart); + const queryEnd = fragmentStart !== -1 ? fragmentStart : url.length; + const queryString = url.slice(queryStart + 1, queryEnd); + + if (queryString.length === 0) return url; + + // FAST PATH: Quick check if any sensitive keywords present + // Using indexOf is faster than regex for simple substring matching + const lower = queryString.toLowerCase(); + const hasSensitive = + lower.includes("token") || + lower.includes("key") || + lower.includes("password") || + lower.includes("passwd") || + lower.includes("secret") || + lower.includes("session") || + lower.includes("auth"); + + if (!hasSensitive) { + return url; + } + + // SLOW PATH: Parse and redact + const redactedParams: string[] = []; + const params = queryString.split("&"); + + for (const param of params) { + const equalIndex = param.indexOf("="); + if (equalIndex === -1) { + redactedParams.push(param); + continue; + } + + const key = param.slice(0, equalIndex); + let shouldRedact = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()); + + if (!shouldRedact && key.includes("%")) { + try { + const decodedKey = decodeURIComponent(key); + shouldRedact = SENSITIVE_QUERY_PARAMS.has(decodedKey.toLowerCase()); + } catch {} + } + + redactedParams.push(shouldRedact ? `${key}=[REDACTED]` : param); + } + + return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd); +} diff --git a/packages/sigarra/src/generated/core/fetcher/requestWithRetries.ts b/packages/sigarra/src/generated/core/fetcher/requestWithRetries.ts new file mode 100644 index 000000000..32dbcd0e8 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/requestWithRetries.ts @@ -0,0 +1,68 @@ +const INITIAL_RETRY_DELAY = 1000; // in milliseconds +const MAX_RETRY_DELAY = 60000; // in milliseconds +const DEFAULT_MAX_RETRIES = 0; +const JITTER_FACTOR = 0.2; // 20% random jitter + +function isRetryableStatusCode(statusCode: number): boolean { + return [408, 429].includes(statusCode) || statusCode >= 500; +} + +function addPositiveJitter(delay: number): number { + const jitterMultiplier = 1 + Math.random() * JITTER_FACTOR; + return delay * jitterMultiplier; +} + +function addSymmetricJitter(delay: number): number { + const jitterMultiplier = 1 + (Math.random() - 0.5) * JITTER_FACTOR; + return delay * jitterMultiplier; +} + +function getRetryDelayFromHeaders(response: Response, retryAttempt: number): number { + const retryAfter = response.headers.get("Retry-After"); + if (retryAfter) { + const retryAfterSeconds = parseInt(retryAfter, 10); + if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) { + return Math.min(retryAfterSeconds * 1000, MAX_RETRY_DELAY); + } + + const retryAfterDate = new Date(retryAfter); + if (!Number.isNaN(retryAfterDate.getTime())) { + const delay = retryAfterDate.getTime() - Date.now(); + if (delay > 0) { + return Math.min(Math.max(delay, 0), MAX_RETRY_DELAY); + } + } + } + + const rateLimitReset = response.headers.get("X-RateLimit-Reset"); + if (rateLimitReset) { + const resetTime = parseInt(rateLimitReset, 10); + if (!Number.isNaN(resetTime)) { + const delay = resetTime * 1000 - Date.now(); + if (delay > 0) { + return addPositiveJitter(Math.min(delay, MAX_RETRY_DELAY)); + } + } + } + + return addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** retryAttempt, MAX_RETRY_DELAY)); +} + +export async function requestWithRetries( + requestFn: () => Promise, + maxRetries: number = DEFAULT_MAX_RETRIES, +): Promise { + let response: Response = await requestFn(); + + for (let i = 0; i < maxRetries; ++i) { + if (isRetryableStatusCode(response.status)) { + const delay = getRetryDelayFromHeaders(response, i); + + await new Promise((resolve) => setTimeout(resolve, delay)); + response = await requestFn(); + } else { + break; + } + } + return response!; +} diff --git a/packages/sigarra/src/generated/core/fetcher/signals.ts b/packages/sigarra/src/generated/core/fetcher/signals.ts new file mode 100644 index 000000000..ba74c4d02 --- /dev/null +++ b/packages/sigarra/src/generated/core/fetcher/signals.ts @@ -0,0 +1,35 @@ +const TIMEOUT = "timeout"; + +export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abortId: ReturnType } { + const controller = new AbortController(); + const abortId = setTimeout(() => controller.abort(TIMEOUT), timeoutMs); + return { signal: controller.signal, abortId }; +} + +export function anySignal(...args: AbortSignal[] | [AbortSignal[]]): AbortSignal { + const signals = (args.length === 1 && Array.isArray(args[0]) ? args[0] : args) as AbortSignal[]; + + const controller = new AbortController(); + + for (const signal of signals) { + if (signal.aborted) { + controller.abort((signal as any)?.reason); + return controller.signal; + } + + signal.addEventListener("abort", () => controller.abort((signal as any)?.reason), { + signal: controller.signal, + }); + + // Re-check after adding listener: the signal may have aborted + // between the initial `signal.aborted` check and the `addEventListener` + // call above. If it did, the abort event was already dispatched and + // the listener will never fire — we must manually abort. + if (signal.aborted) { + controller.abort((signal as any)?.reason); + return controller.signal; + } + } + + return controller.signal; +} diff --git a/packages/sigarra/src/generated/core/headers.ts b/packages/sigarra/src/generated/core/headers.ts new file mode 100644 index 000000000..be45c4552 --- /dev/null +++ b/packages/sigarra/src/generated/core/headers.ts @@ -0,0 +1,33 @@ +export function mergeHeaders(...headersArray: (Record | null | undefined)[]): Record { + const result: Record = {}; + + for (const [key, value] of headersArray + .filter((headers) => headers != null) + .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); + if (value != null) { + result[insensitiveKey] = value; + } else if (insensitiveKey in result) { + delete result[insensitiveKey]; + } + } + + return result; +} + +export function mergeOnlyDefinedHeaders( + ...headersArray: (Record | null | undefined)[] +): Record { + const result: Record = {}; + + for (const [key, value] of headersArray + .filter((headers) => headers != null) + .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); + if (value != null) { + result[insensitiveKey] = value; + } + } + + return result; +} diff --git a/packages/sigarra/src/generated/core/index.ts b/packages/sigarra/src/generated/core/index.ts new file mode 100644 index 000000000..afa8351fc --- /dev/null +++ b/packages/sigarra/src/generated/core/index.ts @@ -0,0 +1,4 @@ +export * from "./fetcher/index.js"; +export * as logging from "./logging/index.js"; +export * from "./runtime/index.js"; +export * as url from "./url/index.js"; diff --git a/packages/sigarra/src/generated/core/json.ts b/packages/sigarra/src/generated/core/json.ts new file mode 100644 index 000000000..c052f3249 --- /dev/null +++ b/packages/sigarra/src/generated/core/json.ts @@ -0,0 +1,27 @@ +/** + * Serialize a value to JSON + * @param value A JavaScript value, usually an object or array, to be converted. + * @param replacer A function that transforms the results. + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + * @returns JSON string + */ +export const toJson = ( + value: unknown, + replacer?: (this: unknown, key: string, value: unknown) => unknown, + space?: string | number, +): string => { + return JSON.stringify(value, replacer, space); +}; + +/** + * Parse JSON string to object, array, or other type + * @param text A valid JSON string. + * @param reviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is. + * @returns Parsed object, array, or other type + */ +export function fromJson( + text: string, + reviver?: (this: unknown, key: string, value: unknown) => unknown, +): T { + return JSON.parse(text, reviver); +} diff --git a/packages/sigarra/src/generated/core/logging/exports.ts b/packages/sigarra/src/generated/core/logging/exports.ts new file mode 100644 index 000000000..88f6c00db --- /dev/null +++ b/packages/sigarra/src/generated/core/logging/exports.ts @@ -0,0 +1,19 @@ +import * as logger from "./logger.js"; + +export namespace logging { + /** + * Configuration for logger instances. + */ + export type LogConfig = logger.LogConfig; + export type LogLevel = logger.LogLevel; + export const LogLevel: typeof logger.LogLevel = logger.LogLevel; + export type ILogger = logger.ILogger; + /** + * Console logger implementation that outputs to the console. + */ + export type ConsoleLogger = logger.ConsoleLogger; + /** + * Console logger implementation that outputs to the console. + */ + export const ConsoleLogger: typeof logger.ConsoleLogger = logger.ConsoleLogger; +} diff --git a/packages/sigarra/src/generated/core/logging/index.ts b/packages/sigarra/src/generated/core/logging/index.ts new file mode 100644 index 000000000..d81cc32c4 --- /dev/null +++ b/packages/sigarra/src/generated/core/logging/index.ts @@ -0,0 +1 @@ +export * from "./logger.js"; diff --git a/packages/sigarra/src/generated/core/logging/logger.ts b/packages/sigarra/src/generated/core/logging/logger.ts new file mode 100644 index 000000000..a3f3673cd --- /dev/null +++ b/packages/sigarra/src/generated/core/logging/logger.ts @@ -0,0 +1,203 @@ +export const LogLevel = { + Debug: "debug", + Info: "info", + Warn: "warn", + Error: "error", +} as const; +export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +const logLevelMap: Record = { + [LogLevel.Debug]: 1, + [LogLevel.Info]: 2, + [LogLevel.Warn]: 3, + [LogLevel.Error]: 4, +}; + +export interface ILogger { + /** + * Logs a debug message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + debug(message: string, ...args: unknown[]): void; + /** + * Logs an info message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + info(message: string, ...args: unknown[]): void; + /** + * Logs a warning message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + warn(message: string, ...args: unknown[]): void; + /** + * Logs an error message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + error(message: string, ...args: unknown[]): void; +} + +/** + * Configuration for logger initialization. + */ +export interface LogConfig { + /** + * Minimum log level to output. + * @default LogLevel.Info + */ + level?: LogLevel; + /** + * Logger implementation to use. + * @default new ConsoleLogger() + */ + logger?: ILogger; + /** + * Whether logging should be silenced. + * @default true + */ + silent?: boolean; +} + +/** + * Default console-based logger implementation. + */ +export class ConsoleLogger implements ILogger { + debug(message: string, ...args: unknown[]): void { + console.debug(message, ...args); + } + info(message: string, ...args: unknown[]): void { + console.info(message, ...args); + } + warn(message: string, ...args: unknown[]): void { + console.warn(message, ...args); + } + error(message: string, ...args: unknown[]): void { + console.error(message, ...args); + } +} + +/** + * Logger class that provides level-based logging functionality. + */ +export class Logger { + private readonly level: number; + private readonly logger: ILogger; + private readonly silent: boolean; + + /** + * Creates a new logger instance. + * @param config - Logger configuration + */ + constructor(config: Required) { + this.level = logLevelMap[config.level]; + this.logger = config.logger; + this.silent = config.silent; + } + + /** + * Checks if a log level should be output based on configuration. + * @param level - The log level to check + * @returns True if the level should be logged + */ + public shouldLog(level: LogLevel): boolean { + return !this.silent && this.level <= logLevelMap[level]; + } + + /** + * Checks if debug logging is enabled. + * @returns True if debug logs should be output + */ + public isDebug(): boolean { + return this.shouldLog(LogLevel.Debug); + } + + /** + * Logs a debug message if debug logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public debug(message: string, ...args: unknown[]): void { + if (this.isDebug()) { + this.logger.debug(message, ...args); + } + } + + /** + * Checks if info logging is enabled. + * @returns True if info logs should be output + */ + public isInfo(): boolean { + return this.shouldLog(LogLevel.Info); + } + + /** + * Logs an info message if info logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public info(message: string, ...args: unknown[]): void { + if (this.isInfo()) { + this.logger.info(message, ...args); + } + } + + /** + * Checks if warning logging is enabled. + * @returns True if warning logs should be output + */ + public isWarn(): boolean { + return this.shouldLog(LogLevel.Warn); + } + + /** + * Logs a warning message if warning logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public warn(message: string, ...args: unknown[]): void { + if (this.isWarn()) { + this.logger.warn(message, ...args); + } + } + + /** + * Checks if error logging is enabled. + * @returns True if error logs should be output + */ + public isError(): boolean { + return this.shouldLog(LogLevel.Error); + } + + /** + * Logs an error message if error logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public error(message: string, ...args: unknown[]): void { + if (this.isError()) { + this.logger.error(message, ...args); + } + } +} + +export function createLogger(config?: LogConfig | Logger): Logger { + if (config == null) { + return defaultLogger; + } + if (config instanceof Logger) { + return config; + } + config = config ?? {}; + config.level ??= LogLevel.Info; + config.logger ??= new ConsoleLogger(); + config.silent ??= true; + return new Logger(config as Required); +} + +const defaultLogger: Logger = new Logger({ + level: LogLevel.Info, + logger: new ConsoleLogger(), + silent: true, +}); diff --git a/packages/sigarra/src/generated/core/requestBody.ts b/packages/sigarra/src/generated/core/requestBody.ts new file mode 100644 index 000000000..d58ef590c --- /dev/null +++ b/packages/sigarra/src/generated/core/requestBody.ts @@ -0,0 +1,26 @@ +/** + * Spreads caller-supplied `additionalBodyParameters` (from `requestOptions.additionalBodyParameters`) + * on top of the request body. Caller-supplied properties win over the endpoint body. When no + * additional body parameters are provided, the original body is returned unchanged so serialization + * is unaffected. + * + * The merge only applies to plain-object (JSON object) bodies. When the body is `null`/`undefined` + * the additional parameters become the body; when the body is an array or a primitive JSON value it + * is returned unchanged, since object properties cannot be spread into it. This mirrors the Python + * SDK, which only merges additional body parameters into mapping bodies. + */ +export function mergeAdditionalBodyParameters( + body: unknown, + additionalBodyParameters: Record | undefined, +): unknown { + if (additionalBodyParameters == null) { + return body; + } + if (body == null) { + return { ...additionalBodyParameters }; + } + if (typeof body === "object" && !Array.isArray(body)) { + return { ...body, ...additionalBodyParameters }; + } + return body; +} diff --git a/packages/sigarra/src/generated/core/runtime/index.ts b/packages/sigarra/src/generated/core/runtime/index.ts new file mode 100644 index 000000000..85a327200 --- /dev/null +++ b/packages/sigarra/src/generated/core/runtime/index.ts @@ -0,0 +1 @@ +export { getUserAgent, RUNTIME } from "./runtime.js"; diff --git a/packages/sigarra/src/generated/core/runtime/runtime.ts b/packages/sigarra/src/generated/core/runtime/runtime.ts new file mode 100644 index 000000000..d367ce6b9 --- /dev/null +++ b/packages/sigarra/src/generated/core/runtime/runtime.ts @@ -0,0 +1,231 @@ +interface DenoGlobal { + version: { + deno: string; + }; + build?: { + os?: string; + arch?: string; + }; +} + +interface BunGlobal { + version: string; +} + +declare const Deno: DenoGlobal | undefined; +declare const Bun: BunGlobal | undefined; +declare const EdgeRuntime: string | undefined; +declare const self: typeof globalThis.self & { + importScripts?: unknown; +}; + +/** + * A constant that indicates which environment and version the SDK is running in. + */ +export const RUNTIME: Runtime = evaluateRuntime(); + +export interface Runtime { + type: "browser" | "web-worker" | "deno" | "bun" | "node" | "react-native" | "unknown" | "workerd" | "edge-runtime"; + version?: string; + parsedVersion?: number; + /** + * The operating system the SDK is running on, when it can be determined + * (e.g. "linux", "darwin", "win32" on server runtimes). Undefined in + * environments where the OS is not observable (e.g. browsers). + */ + os?: string; + /** + * The CPU architecture the SDK is running on, when it can be determined + * (e.g. "x64", "arm64" on server runtimes). Undefined in environments where + * the architecture is not observable (e.g. browsers). + */ + arch?: string; +} + +function evaluateRuntime(): Runtime { + /** + * A constant that indicates whether the environment the code is running is a Web Browser. + */ + const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; + if (isBrowser) { + return { + type: "browser", + version: window.navigator.userAgent, + }; + } + + /** + * A constant that indicates whether the environment the code is running is Cloudflare. + * https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent + */ + const isCloudflare = typeof globalThis !== "undefined" && globalThis?.navigator?.userAgent === "Cloudflare-Workers"; + if (isCloudflare) { + return { + type: "workerd", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Edge Runtime. + * https://vercel.com/docs/functions/runtimes/edge-runtime#check-if-you're-running-on-the-edge-runtime + */ + const isEdgeRuntime = typeof EdgeRuntime === "string"; + if (isEdgeRuntime) { + return { + type: "edge-runtime", + }; + } + + /** + * A constant that indicates whether the environment the code is running is a Web Worker. + */ + const isWebWorker = + typeof self === "object" && + typeof self?.importScripts === "function" && + (self.constructor?.name === "DedicatedWorkerGlobalScope" || + self.constructor?.name === "ServiceWorkerGlobalScope" || + self.constructor?.name === "SharedWorkerGlobalScope"); + if (isWebWorker) { + return { + type: "web-worker", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Deno. + * FYI Deno spoofs process.versions.node, see https://deno.land/std@0.177.0/node/process.ts?s=versions + */ + const isDeno = + typeof Deno !== "undefined" && typeof Deno.version !== "undefined" && typeof Deno.version.deno !== "undefined"; + if (isDeno) { + return { + type: "deno", + version: Deno.version.deno, + os: Deno.build?.os, + arch: Deno.build?.arch, + }; + } + + /** + * A constant that indicates whether the environment the code is running is Bun.sh. + */ + const isBun = typeof Bun !== "undefined" && typeof Bun.version !== "undefined"; + if (isBun) { + return { + type: "bun", + version: Bun.version, + os: typeof process !== "undefined" ? process.platform : undefined, + arch: typeof process !== "undefined" ? process.arch : undefined, + }; + } + + /** + * A constant that indicates whether the environment the code is running is in React-Native. + * This check should come before Node.js detection since React Native may have a process polyfill. + * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js + */ + const isReactNative = typeof navigator !== "undefined" && navigator?.product === "ReactNative"; + if (isReactNative) { + return { + type: "react-native", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Node.JS. + * + * We assign `process` to a local variable first to avoid being flagged by + * bundlers that perform static analysis on `process.versions` (e.g. Next.js + * Edge Runtime warns about Node.js APIs even when they are guarded). + */ + const _process = typeof process !== "undefined" ? process : undefined; + const isNode = typeof _process !== "undefined" && typeof _process.versions?.node === "string"; + if (isNode) { + return { + type: "node", + version: _process.versions.node, + parsedVersion: Number(_process.versions.node.split(".")[0]), + os: _process.platform, + arch: _process.arch, + }; + } + + return { + type: "unknown", + }; +} + +/** + * Display names for the language runtimes whose version is meaningful to encode + * in a User-Agent. Environments where a version string is not useful (e.g. + * browsers, where `version` is the full navigator UA) are intentionally mapped + * to `undefined` so they are omitted from the User-Agent. + */ +const RUNTIME_DISPLAY_NAMES: Record = { + node: "Node", + deno: "Deno", + bun: "Bun", + browser: undefined, + "web-worker": undefined, + "react-native": undefined, + workerd: undefined, + "edge-runtime": undefined, + unknown: undefined, +}; + +/** + * CPU architecture aliases that all refer to 64-bit x86. They are normalized to + * the single canonical token `x86_64` so the User-Agent architecture label is + * consistent regardless of which runtime reports it (Node reports `x64`, others + * report `amd64` or `x86_64`). + */ +const X86_64_ARCH_ALIASES = new Set(["x64", "amd64", "x86_64"]); + +/** + * Normalizes a CPU architecture token, collapsing the 64-bit x86 aliases + * (`x64`, `amd64`, `x86_64`) to `x86_64`. Other architectures are returned + * unchanged. + */ +function normalizeArch(arch: string | undefined): string | undefined { + if (arch == null) { + return arch; + } + return X86_64_ARCH_ALIASES.has(arch.toLowerCase()) ? "x86_64" : arch; +} + +/** + * Percent-encodes the `@` and `/` characters in an npm package name so the + * User-Agent product token stays within the RFC 7230 token grammar. The + * original scoped package name can be recovered by URL-decoding (e.g. + * `@dummy/sdk` becomes `%40dummy%2Fsdk`). + */ +function encodeProductName(sdkName: string): string { + return sdkName.replace(/@/g, "%40").replace(/\//g, "%2F"); +} + +/** + * Builds a structured User-Agent string of the form + * `{sdkName}/{sdkVersion} ({os}; {arch}) {runtime}/{runtimeVersion}` + * where the platform group and runtime segment are omitted gracefully when the + * underlying values cannot be determined (e.g. in a browser). + */ +export function getUserAgent(sdkName: string, sdkVersion: string): string { + let userAgent = `${encodeProductName(sdkName)}/${sdkVersion}`; + + const platform = [RUNTIME.os, normalizeArch(RUNTIME.arch)].filter( + (part): part is string => part != null && part.length > 0, + ); + if (platform.length > 0) { + userAgent += ` (${platform.join("; ")})`; + } + + const runtimeName = RUNTIME_DISPLAY_NAMES[RUNTIME.type]; + if (runtimeName != null) { + userAgent += ` ${runtimeName}`; + if (RUNTIME.version != null && RUNTIME.version.length > 0) { + userAgent += `/${RUNTIME.version}`; + } + } + + return userAgent; +} diff --git a/packages/sigarra/src/generated/core/url/QueryStringBuilder.ts b/packages/sigarra/src/generated/core/url/QueryStringBuilder.ts new file mode 100644 index 000000000..b045221c3 --- /dev/null +++ b/packages/sigarra/src/generated/core/url/QueryStringBuilder.ts @@ -0,0 +1,87 @@ +import { toQueryString } from "./qs.js"; + +/** + * Creates a fluent builder for constructing URL query strings. + * + * Each `.add()` call serializes its value immediately (like C#'s builder), + * so no format tracking is needed — the style is applied at add-time. + * + * Usage (generated code): + * + * const qs = core.url.queryBuilder() + * .add("limit", limit) + * .add("tags", tags, { style: "comma" }) // explode: false + * .mergeAdditional(requestOptions?.queryParams) + * .build(); + */ +export function queryBuilder(): QueryStringBuilder { + return new QueryStringBuilder(); +} + +class QueryStringBuilder { + private parts: Map = new Map(); + + /** + * Adds a query parameter, serializing it immediately. + * + * By default arrays use "repeat" format (`key=a&key=b`). + * Pass `{ style: "comma" }` for OpenAPI `explode: false` parameters + * to get comma-separated values (`key=a,b,c`). + * + * Null / undefined values are silently skipped. + */ + add(key: string, value: unknown, options?: { style?: "comma" }): this { + if (value === undefined || value === null) { + return this; + } + const serialized = toQueryString( + { [key]: value }, + { arrayFormat: options?.style === "comma" ? "comma" : "repeat" }, + ); + if (serialized.length > 0) { + this.parts.set(key, serialized); + } + return this; + } + + /** + * Adds multiple query parameters at once from a record. + * All parameters use the default "repeat" array format. + * Null / undefined values are silently skipped. + */ + addMany(params: Record): this { + if (params != null) { + for (const [key, value] of Object.entries(params)) { + this.add(key, value); + } + } + return this; + } + + /** + * Merges additional query parameters supplied at call-time via + * `requestOptions.queryParams`. Overrides existing keys (last-write-wins). + */ + mergeAdditional(additionalParams?: Record): this { + if (additionalParams != null) { + for (const [key, value] of Object.entries(additionalParams)) { + if (value === undefined || value === null) { + continue; + } + const serialized = toQueryString({ [key]: value }, { arrayFormat: "repeat" }); + if (serialized.length > 0) { + this.parts.set(key, serialized); + } + } + } + return this; + } + + /** + * Returns the assembled query string (without the leading `?`). + * Returns an empty string when no parameters were added. + */ + build(): string { + return [...this.parts.values()].join("&"); + } +} diff --git a/packages/sigarra/src/generated/core/url/encodePathParam.ts b/packages/sigarra/src/generated/core/url/encodePathParam.ts new file mode 100644 index 000000000..19b901244 --- /dev/null +++ b/packages/sigarra/src/generated/core/url/encodePathParam.ts @@ -0,0 +1,18 @@ +export function encodePathParam(param: unknown): string { + if (param === null) { + return "null"; + } + const typeofParam = typeof param; + switch (typeofParam) { + case "undefined": + return "undefined"; + case "string": + case "number": + case "boolean": + break; + default: + param = String(param); + break; + } + return encodeURIComponent(param as string | number | boolean); +} diff --git a/packages/sigarra/src/generated/core/url/index.ts b/packages/sigarra/src/generated/core/url/index.ts new file mode 100644 index 000000000..ca9d4fbd1 --- /dev/null +++ b/packages/sigarra/src/generated/core/url/index.ts @@ -0,0 +1,4 @@ +export { encodePathParam } from "./encodePathParam.js"; +export { join } from "./join.js"; +export { queryBuilder } from "./QueryStringBuilder.js"; +export { toQueryString } from "./qs.js"; diff --git a/packages/sigarra/src/generated/core/url/join.ts b/packages/sigarra/src/generated/core/url/join.ts new file mode 100644 index 000000000..7ca7daef0 --- /dev/null +++ b/packages/sigarra/src/generated/core/url/join.ts @@ -0,0 +1,79 @@ +export function join(base: string, ...segments: string[]): string { + if (!base) { + return ""; + } + + if (segments.length === 0) { + return base; + } + + if (base.includes("://")) { + let url: URL; + try { + url = new URL(base); + } catch { + return joinPath(base, ...segments); + } + + const lastSegment = segments[segments.length - 1]; + const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + + for (const segment of segments) { + const cleanSegment = trimSlashes(segment); + if (cleanSegment) { + url.pathname = joinPathSegments(url.pathname, cleanSegment); + } + } + + if (shouldPreserveTrailingSlash && !url.pathname.endsWith("/")) { + url.pathname += "/"; + } + + return url.toString(); + } + + return joinPath(base, ...segments); +} + +function joinPath(base: string, ...segments: string[]): string { + if (segments.length === 0) { + return base; + } + + let result = base; + + const lastSegment = segments[segments.length - 1]; + const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + + for (const segment of segments) { + const cleanSegment = trimSlashes(segment); + if (cleanSegment) { + result = joinPathSegments(result, cleanSegment); + } + } + + if (shouldPreserveTrailingSlash && !result.endsWith("/")) { + result += "/"; + } + + return result; +} + +function joinPathSegments(left: string, right: string): string { + if (left.endsWith("/")) { + return left + right; + } + return `${left}/${right}`; +} + +function trimSlashes(str: string): string { + if (!str) return str; + + let start = 0; + let end = str.length; + + if (str.startsWith("/")) start = 1; + if (str.endsWith("/")) end = str.length - 1; + + return start === 0 && end === str.length ? str : str.slice(start, end); +} diff --git a/packages/sigarra/src/generated/core/url/qs.ts b/packages/sigarra/src/generated/core/url/qs.ts new file mode 100644 index 000000000..aebb95a38 --- /dev/null +++ b/packages/sigarra/src/generated/core/url/qs.ts @@ -0,0 +1,87 @@ +type ArrayFormat = "indices" | "repeat" | "comma"; + +interface QueryStringOptions { + arrayFormat?: ArrayFormat; + encode?: boolean; +} + +const defaultQsOptions: Required = { + arrayFormat: "indices", + encode: true, +} as const; + +function encodeValue(value: unknown, shouldEncode: boolean): string { + if (value === undefined) { + return ""; + } + if (value === null) { + return ""; + } + const stringValue = String(value); + return shouldEncode ? encodeURIComponent(stringValue) : stringValue; +} + +function stringifyObject(obj: Record, prefix = "", options: Required): string[] { + const parts: string[] = []; + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}[${key}]` : key; + + if (value == null) { + continue; + } + + if (Array.isArray(value)) { + if (value.length === 0) { + continue; + } + const effectiveFormat = options.arrayFormat; + if (effectiveFormat === "comma") { + const encodedKey = options.encode ? encodeURIComponent(fullKey) : fullKey; + const encodedValues = value + .filter((item) => item !== undefined && item !== null) + .map((item) => encodeValue(item, options.encode)); + if (encodedValues.length > 0) { + parts.push(`${encodedKey}=${encodedValues.join(",")}`); + } + } else { + for (let i = 0; i < value.length; i++) { + const item = value[i]; + if (item == null) { + continue; + } + if (typeof item === "object" && !Array.isArray(item) && item !== null) { + const arrayKey = effectiveFormat === "indices" ? `${fullKey}[${i}]` : fullKey; + parts.push(...stringifyObject(item as Record, arrayKey, options)); + } else { + const arrayKey = effectiveFormat === "indices" ? `${fullKey}[${i}]` : fullKey; + const encodedKey = options.encode ? encodeURIComponent(arrayKey) : arrayKey; + parts.push(`${encodedKey}=${encodeValue(item, options.encode)}`); + } + } + } + } else if (typeof value === "object" && value !== null) { + if (Object.keys(value as Record).length === 0) { + continue; + } + parts.push(...stringifyObject(value as Record, fullKey, options)); + } else { + const encodedKey = options.encode ? encodeURIComponent(fullKey) : fullKey; + parts.push(`${encodedKey}=${encodeValue(value, options.encode)}`); + } + } + + return parts; +} + +export function toQueryString(obj: unknown, options?: QueryStringOptions): string { + if (obj == null || typeof obj !== "object") { + return ""; + } + + const parts = stringifyObject(obj as Record, "", { + ...defaultQsOptions, + ...options, + }); + return parts.join("&"); +} diff --git a/packages/sigarra/src/generated/environments.ts b/packages/sigarra/src/generated/environments.ts new file mode 100644 index 000000000..b6fd799dd --- /dev/null +++ b/packages/sigarra/src/generated/environments.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +export const SigarraEnvironment = { + Default: "https://sigarra.up.pt", +} as const; + +export type SigarraEnvironment = typeof SigarraEnvironment.Default; diff --git a/packages/sigarra/src/generated/errors/SigarraError.ts b/packages/sigarra/src/generated/errors/SigarraError.ts new file mode 100644 index 000000000..dd7f5d6ee --- /dev/null +++ b/packages/sigarra/src/generated/errors/SigarraError.ts @@ -0,0 +1,68 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../core/index.js"; +import { toJson } from "../core/json.js"; + +export class SigarraError extends Error { + public readonly statusCode?: number; + public readonly body?: unknown; + public readonly rawResponse?: core.RawResponse; + public readonly cause?: unknown; + + constructor({ + message, + statusCode, + body, + rawResponse, + cause, + }: { + message?: string; + statusCode?: number; + body?: unknown; + rawResponse?: core.RawResponse; + cause?: unknown; + }) { + super(buildMessage({ message, statusCode, body })); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "SigarraError"; + this.statusCode = statusCode; + this.body = body; + this.rawResponse = rawResponse; + if (cause != null) { + this.cause = cause; + } + } + + public get requestId(): string | undefined { + return this.rawResponse?.headers?.get("x-request-id") ?? undefined; + } +} + +function buildMessage({ + message, + statusCode, + body, +}: { + message: string | undefined; + statusCode: number | undefined; + body: unknown | undefined; +}): string { + const lines: string[] = []; + if (message != null) { + lines.push(message); + } + + if (statusCode != null) { + lines.push(`Status code: ${statusCode.toString()}`); + } + + if (body != null) { + lines.push(`Body: ${toJson(body, undefined, 2)}`); + } + + return lines.join("\n"); +} diff --git a/packages/sigarra/src/generated/errors/SigarraTimeoutError.ts b/packages/sigarra/src/generated/errors/SigarraTimeoutError.ts new file mode 100644 index 000000000..646f9996c --- /dev/null +++ b/packages/sigarra/src/generated/errors/SigarraTimeoutError.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as errors from "./index.js"; + +export class SigarraTimeoutError extends errors.SigarraError { + constructor(message: string, opts?: { cause?: unknown }) { + super({ + message: message, + cause: opts?.cause, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "SigarraTimeoutError"; + } +} diff --git a/packages/sigarra/src/generated/errors/handleNonStatusCodeError.ts b/packages/sigarra/src/generated/errors/handleNonStatusCodeError.ts new file mode 100644 index 000000000..b9259b5c3 --- /dev/null +++ b/packages/sigarra/src/generated/errors/handleNonStatusCodeError.ts @@ -0,0 +1,40 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../core/index.js"; +import * as errors from "./index.js"; + +export function handleNonStatusCodeError( + error: core.Fetcher.Error, + rawResponse: core.RawResponse, + method: string, + path: string, +): never { + switch (error.reason) { + case "non-json": + throw new errors.SigarraError({ + statusCode: error.statusCode, + body: error.rawBody, + rawResponse: rawResponse, + }); + case "body-is-null": + throw new errors.SigarraError({ + statusCode: error.statusCode, + rawResponse: rawResponse, + }); + case "timeout": + throw new errors.SigarraTimeoutError(`Timeout exceeded when calling ${method} ${path}.`, { + cause: error.cause, + }); + case "unknown": + throw new errors.SigarraError({ + message: error.errorMessage, + rawResponse: rawResponse, + cause: error.cause, + }); + default: + throw new errors.SigarraError({ + message: "Unknown error", + rawResponse: rawResponse, + }); + } +} diff --git a/packages/sigarra/src/generated/errors/index.ts b/packages/sigarra/src/generated/errors/index.ts new file mode 100644 index 000000000..40559643d --- /dev/null +++ b/packages/sigarra/src/generated/errors/index.ts @@ -0,0 +1,2 @@ +export { SigarraError } from "./SigarraError.js"; +export { SigarraTimeoutError } from "./SigarraTimeoutError.js"; diff --git a/packages/sigarra/src/generated/exports.ts b/packages/sigarra/src/generated/exports.ts new file mode 100644 index 000000000..7b70ee14f --- /dev/null +++ b/packages/sigarra/src/generated/exports.ts @@ -0,0 +1 @@ +export * from "./core/exports.js"; diff --git a/packages/sigarra/src/generated/index.ts b/packages/sigarra/src/generated/index.ts new file mode 100644 index 000000000..01d7b1920 --- /dev/null +++ b/packages/sigarra/src/generated/index.ts @@ -0,0 +1,6 @@ +export * as Sigarra from "./api/index.js"; +export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; +export { SigarraClient } from "./Client.js"; +export { SigarraEnvironment } from "./environments.js"; +export { SigarraError, SigarraTimeoutError } from "./errors/index.js"; +export * from "./exports.js"; diff --git a/packages/sigarra/src/index.ts b/packages/sigarra/src/index.ts new file mode 100644 index 000000000..963870b8a --- /dev/null +++ b/packages/sigarra/src/index.ts @@ -0,0 +1 @@ +export * from './generated/index.ts' diff --git a/packages/sigarra/tsconfig.json b/packages/sigarra/tsconfig.json new file mode 100644 index 000000000..b7c5e9e0a --- /dev/null +++ b/packages/sigarra/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "noEmit": true, + "target": "ES2022", + "module": "preserve", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + } +}