From 7e7b3afb1a7523b86b9a9a39a95afff478a250a4 Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Wed, 24 Jun 2026 15:07:54 +0500 Subject: [PATCH 1/8] chore: add SDK generation tooling and README Adds the tools needed to generate the SDK from openedx-platform: - filter_schema.py: filters the full OpenAPI schema to only paths tagged with openedx-platform-sdk, resolving all transitive $ref dependencies - config.yml: openapi-python-client config (package/project name + include_tags) - README.md: documents covered APIs, regeneration steps (curl + filter + openapi-python-client generate), and usage examples Co-Authored-By: Claude Sonnet 4.6 --- README.md | 157 ++++++++++++++++++++++++++++++++++++++++++++++- config.yml | 4 ++ filter_schema.py | 95 ++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 config.yml create mode 100644 filter_schema.py diff --git a/README.md b/README.md index 719ff2c..3bb0f73 100644 --- a/README.md +++ b/README.md @@ -1 +1,156 @@ -# openedx-platform-sdk \ No newline at end of file +# openedx-platform-sdk + +A Python client library for the [OpenedX Authoring API](https://docs.openedx.org), auto-generated from the platform's OpenAPI schema using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client). + +Covers the standardized v1/v3/v4 APIs tagged `openedx-platform-sdk` in the platform. + +--- + +## Covered APIs + +| API | Version | Operations | +|-----|---------|-----------| +| XBlock | v1 | create, retrieve, update, partial_update, destroy | +| Authoring Grading | v3 | partial_update | +| Course Details | v3 | retrieve, update | +| Home | v3 | list, courses, libraries | +| Home (paginated) | v4 | courses | + +--- + +## Regenerating the SDK + +The SDK is generated from the platform's OpenAPI schema. Run this whenever APIs change: + +### Prerequisites + +```bash +pip install openapi-python-client pyyaml +``` + +### Steps + +```bash +# 1. Download the schema from a running Studio instance +curl http://studio.local.openedx.io:8001/authoring-api/schema/ > schema.yml + +# 2. Filter schema to only openedx-platform-sdk tagged paths +python filter_schema.py schema.yml filtered_schema.yml openedx-platform-sdk + +# 3. Regenerate the SDK +openapi-python-client generate \ + --path filtered_schema.yml \ + --config config.yml +``` + +Or use the provided script (supports optional branch checkout): + +```bash +./regen_sdk.sh # uses current platform branch +./regen_sdk.sh feat/axim-api_improvements # checkout branch first, then regenerate +``` + +--- + +## Authentication + +OpenedX Studio uses JWT tokens via the OAuth2 `client_credentials` flow. + +```python +from openedx_platform_sdk import OAuth2ClientCredentials + +auth = OAuth2ClientCredentials( + lms_url="http://localhost:18000", + client_id="your-client-id", + client_secret="your-client-secret", +) + +# Get a ready-to-use authenticated client (token fetched automatically) +with auth.get_client(studio_url="http://localhost:18010/api/contentstore") as client: + from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list + result = v3_home_list.sync(client=client) +``` + +Token is cached and auto-refreshed 60 seconds before expiry. The `JWT` prefix is used automatically (required by OpenedX). + +--- + +## Usage + +### Basic authenticated client + +```python +from openedx_platform_sdk import AuthenticatedClient + +client = AuthenticatedClient( + base_url="http://localhost:18010/api/contentstore", + token="your-jwt-token", + prefix="JWT", +) +``` + +### Call an endpoint + +```python +from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list +from openedx_platform_sdk.api.openedx_platform_sdk import v3_course_details_retrieve +from openedx_platform_sdk.api.openedx_platform_sdk import v4_home_courses_list + +with client as client: + # Get studio home + home = v3_home_list.sync(client=client) + + # Get course details + details = v3_course_details_retrieve.sync( + client=client, + course_id="course-v1:org+course+run", + ) + + # Get paginated courses (v4) + courses = v4_home_courses_list.sync(client=client, page=1, page_size=20) +``` + +### Async support + +```python +import asyncio +from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list + +async def main(): + async with client as c: + home = await v3_home_list.asyncio(client=c) + +asyncio.run(main()) +``` + +### Detailed response (status code, headers) + +```python +from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list +from openedx_platform_sdk.types import Response + +with client as client: + response: Response = v3_home_list.sync_detailed(client=client) + print(response.status_code) + print(response.parsed) +``` + +--- + +## Installation + +```bash +# From source +pip install . + +# With Poetry +poetry install +``` + +--- + +## Publishing + +```bash +poetry publish --build +``` diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..be26c24 --- /dev/null +++ b/config.yml @@ -0,0 +1,4 @@ +project_name_override: openedx-platform-sdk +package_name_override: openedx_platform_sdk +include_tags: + - openedx-platform-sdk diff --git a/filter_schema.py b/filter_schema.py new file mode 100644 index 0000000..07b3c4f --- /dev/null +++ b/filter_schema.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Filter an OpenAPI schema to only include paths tagged with a specific tag. +Recursively resolves all $ref dependencies so the filtered schema is self-contained. + +Usage: + python filter_schema.py schema.yml filtered_schema.yml openedx-platform-sdk +""" + +import sys +import yaml + + +def collect_refs(obj, refs: set) -> None: + """Recursively collect all $ref strings from a schema object.""" + if isinstance(obj, dict): + if "$ref" in obj: + ref = obj["$ref"] + # e.g. '#/components/schemas/Xblock' -> 'Xblock' + if ref.startswith("#/components/schemas/"): + refs.add(ref.split("/")[-1]) + for v in obj.values(): + collect_refs(v, refs) + elif isinstance(obj, list): + for item in obj: + collect_refs(item, refs) + + +def resolve_all_refs(names: set, components: dict) -> set: + """Expand a set of schema names to include all transitive $ref dependencies.""" + resolved = set() + queue = list(names) + while queue: + name = queue.pop() + if name in resolved: + continue + resolved.add(name) + schema_def = components.get(name) + if schema_def: + nested = set() + collect_refs(schema_def, nested) + for dep in nested: + if dep not in resolved: + queue.append(dep) + return resolved + + +def filter_schema(input_path: str, output_path: str, tag: str) -> None: + with open(input_path) as f: + schema = yaml.safe_load(f) + + # 1. Filter paths to only tagged operations + filtered_paths = {} + for path, path_item in schema.get("paths", {}).items(): + filtered_methods = {} + for method, operation in path_item.items(): + if not isinstance(operation, dict): + continue + if tag in operation.get("tags", []): + filtered_methods[method] = operation + if filtered_methods: + filtered_paths[path] = filtered_methods + + schema["paths"] = filtered_paths + + # 2. Collect all $refs used directly in filtered paths + direct_refs: set = set() + collect_refs(filtered_paths, direct_refs) + + # 3. Recursively resolve transitive dependencies + all_components = schema.get("components", {}).get("schemas", {}) + all_needed = resolve_all_refs(direct_refs, all_components) + + # 4. Keep only needed components + if "components" in schema and "schemas" in schema["components"]: + schema["components"]["schemas"] = { + k: v for k, v in schema["components"]["schemas"].items() + if k in all_needed + } + + with open(output_path, "w") as f: + yaml.dump(schema, f, allow_unicode=True, sort_keys=False) + + print(f"Filtered schema written to: {output_path}") + print(f"Paths kept: {len(filtered_paths)}") + for path in filtered_paths: + print(f" {path}") + print(f"Components kept: {len(all_needed)} (from {len(all_components)} total)") + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + filter_schema(sys.argv[1], sys.argv[2], sys.argv[3]) From e75ddadbee9043fa0202a0ba2d8e1854dc0663bb Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Wed, 24 Jun 2026 15:11:34 +0500 Subject: [PATCH 2/8] feat: add auto-generated SDK from openedx-platform-sdk tagged endpoints Generated with openapi-python-client from the filtered OpenAPI schema. Covers 12 operations across 5 APIs: - XBlock v1: create, retrieve, update, partial_update, destroy - Authoring Grading v3: partial_update - Course Details v3: retrieve, update - Home v3: list, courses, libraries - Home v4: courses (paginated) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 23 + openedx_platform_sdk/__init__.py | 8 + openedx_platform_sdk/api/__init__.py | 1 + .../api/openedx_platform_sdk/__init__.py | 1 + .../openedx_platform_sdk/v1_xblock_create.py | 322 ++++ .../openedx_platform_sdk/v1_xblock_destroy.py | 124 ++ .../v1_xblock_partial_update.py | 339 ++++ .../v1_xblock_retrieve.py | 203 +++ .../openedx_platform_sdk/v1_xblock_update.py | 338 ++++ .../v3_authoring_grading_partial_update.py | 242 +++ .../v3_course_details_retrieve.py | 220 +++ .../v3_course_details_update.py | 211 +++ .../v3_home_courses_retrieve.py | 140 ++ .../v3_home_libraries_retrieve.py | 140 ++ .../api/openedx_platform_sdk/v3_home_list.py | 149 ++ .../v4_home_courses_list.py | 286 ++++ openedx_platform_sdk/client.py | 268 ++++ openedx_platform_sdk/errors.py | 16 + openedx_platform_sdk/models/__init__.py | 63 + .../authoring_grading_course_grading_v0.py | 95 ++ .../models/authoring_grading_graders_v0.py | 116 ++ openedx_platform_sdk/models/course_common.py | 118 ++ .../models/course_common_serializer_v2.py | 134 ++ openedx_platform_sdk/models/course_details.py | 576 +++++++ .../models/course_home_tab.py | 143 ++ .../models/course_home_tab_serializer_v2.py | 123 ++ .../models/instructor_info.py | 100 ++ openedx_platform_sdk/models/instructors.py | 99 ++ openedx_platform_sdk/models/library_tab.py | 98 ++ openedx_platform_sdk/models/library_view.py | 148 ++ openedx_platform_sdk/models/patched_xblock.py | 1397 +++++++++++++++++ .../models/patched_xblock_actions_type_0.py | 47 + .../models/patched_xblock_fields_type_0.py | 47 + .../patched_xblock_group_access_type_0.py | 47 + .../models/patched_xblock_metadata_type_0.py | 47 + ...tched_xblock_user_partition_info_type_0.py | 47 + ...chedauthoring_grading_course_grading_v0.py | 121 ++ openedx_platform_sdk/models/publish_enum.py | 10 + openedx_platform_sdk/models/studio_home.py | 380 +++++ .../models/unsucceeded_course.py | 118 ++ .../unsucceeded_course_serializer_v2.py | 118 ++ .../models/v3_course_details_retrieve_view.py | 8 + openedx_platform_sdk/models/xblock.py | 1397 +++++++++++++++++ .../models/xblock_actions_type_0.py | 47 + .../models/xblock_fields_type_0.py | 47 + .../models/xblock_group_access_type_0.py | 47 + .../models/xblock_metadata_type_0.py | 47 + .../xblock_user_partition_info_type_0.py | 47 + openedx_platform_sdk/py.typed | 1 + openedx_platform_sdk/types.py | 54 + pyproject.toml | 29 + 51 files changed, 8947 insertions(+) create mode 100644 .gitignore create mode 100644 openedx_platform_sdk/__init__.py create mode 100644 openedx_platform_sdk/api/__init__.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/__init__.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_retrieve.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v3_home_courses_retrieve.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v3_home_libraries_retrieve.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v3_home_list.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_list.py create mode 100644 openedx_platform_sdk/client.py create mode 100644 openedx_platform_sdk/errors.py create mode 100644 openedx_platform_sdk/models/__init__.py create mode 100644 openedx_platform_sdk/models/authoring_grading_course_grading_v0.py create mode 100644 openedx_platform_sdk/models/authoring_grading_graders_v0.py create mode 100644 openedx_platform_sdk/models/course_common.py create mode 100644 openedx_platform_sdk/models/course_common_serializer_v2.py create mode 100644 openedx_platform_sdk/models/course_details.py create mode 100644 openedx_platform_sdk/models/course_home_tab.py create mode 100644 openedx_platform_sdk/models/course_home_tab_serializer_v2.py create mode 100644 openedx_platform_sdk/models/instructor_info.py create mode 100644 openedx_platform_sdk/models/instructors.py create mode 100644 openedx_platform_sdk/models/library_tab.py create mode 100644 openedx_platform_sdk/models/library_view.py create mode 100644 openedx_platform_sdk/models/patched_xblock.py create mode 100644 openedx_platform_sdk/models/patched_xblock_actions_type_0.py create mode 100644 openedx_platform_sdk/models/patched_xblock_fields_type_0.py create mode 100644 openedx_platform_sdk/models/patched_xblock_group_access_type_0.py create mode 100644 openedx_platform_sdk/models/patched_xblock_metadata_type_0.py create mode 100644 openedx_platform_sdk/models/patched_xblock_user_partition_info_type_0.py create mode 100644 openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py create mode 100644 openedx_platform_sdk/models/publish_enum.py create mode 100644 openedx_platform_sdk/models/studio_home.py create mode 100644 openedx_platform_sdk/models/unsucceeded_course.py create mode 100644 openedx_platform_sdk/models/unsucceeded_course_serializer_v2.py create mode 100644 openedx_platform_sdk/models/v3_course_details_retrieve_view.py create mode 100644 openedx_platform_sdk/models/xblock.py create mode 100644 openedx_platform_sdk/models/xblock_actions_type_0.py create mode 100644 openedx_platform_sdk/models/xblock_fields_type_0.py create mode 100644 openedx_platform_sdk/models/xblock_group_access_type_0.py create mode 100644 openedx_platform_sdk/models/xblock_metadata_type_0.py create mode 100644 openedx_platform_sdk/models/xblock_user_partition_info_type_0.py create mode 100644 openedx_platform_sdk/py.typed create mode 100644 openedx_platform_sdk/types.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79a2c3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +__pycache__/ +build/ +dist/ +*.egg-info/ +.pytest_cache/ + +# pyenv +.python-version + +# Environments +.env +.venv + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# JetBrains +.idea/ + +/coverage.xml +/.coverage diff --git a/openedx_platform_sdk/__init__.py b/openedx_platform_sdk/__init__.py new file mode 100644 index 0000000..11180d8 --- /dev/null +++ b/openedx_platform_sdk/__init__.py @@ -0,0 +1,8 @@ +"""A client library for accessing Authoring API""" + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/openedx_platform_sdk/api/__init__.py b/openedx_platform_sdk/api/__init__.py new file mode 100644 index 0000000..81f9fa2 --- /dev/null +++ b/openedx_platform_sdk/api/__init__.py @@ -0,0 +1 @@ +"""Contains methods for accessing the API""" diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/__init__.py b/openedx_platform_sdk/api/openedx_platform_sdk/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py new file mode 100644 index 0000000..84dd59e --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py @@ -0,0 +1,322 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.xblock import Xblock +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/xblock/", + } + + if isinstance(body, Xblock): + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + if isinstance(body, Xblock): + if not isinstance(body, Unset): + _kwargs["data"] = body.to_dict() + headers["Content-Type"] = "application/x-www-form-urlencoded" + if isinstance(body, Xblock): + if not isinstance(body, Unset): + _kwargs["files"] = body.to_multipart() + + headers["Content-Type"] = "multipart/form-data; boundary=+++" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: + if response.status_code == 201: + response_201 = Xblock.from_dict(response.json()) + + return response_201 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py new file mode 100644 index 0000000..fb54437 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py @@ -0,0 +1,124 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs( + usage_key_string: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/v1/xblock/{usage_key_string}/".format( + usage_key_string=quote(str(usage_key_string), safe=""), + ), + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: + if response.status_code == 204: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py new file mode 100644 index 0000000..26cdfe7 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py @@ -0,0 +1,339 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.patched_xblock import PatchedXblock +from ...models.xblock import Xblock +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + usage_key_string: str, + *, + body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/v1/xblock/{usage_key_string}/".format( + usage_key_string=quote(str(usage_key_string), safe=""), + ), + } + + if isinstance(body, PatchedXblock): + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + if isinstance(body, PatchedXblock): + if not isinstance(body, Unset): + _kwargs["data"] = body.to_dict() + headers["Content-Type"] = "application/x-www-form-urlencoded" + if isinstance(body, PatchedXblock): + if not isinstance(body, Unset): + _kwargs["files"] = body.to_multipart() + + headers["Content-Type"] = "multipart/form-data; boundary=+++" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: + if response.status_code == 200: + response_200 = Xblock.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return sync_detailed( + usage_key_string=usage_key_string, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (PatchedXblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return ( + await asyncio_detailed( + usage_key_string=usage_key_string, + client=client, + body=body, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py new file mode 100644 index 0000000..16aa531 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py @@ -0,0 +1,203 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.xblock import Xblock +from ...types import Response + + +def _get_kwargs( + usage_key_string: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v1/xblock/{usage_key_string}/".format( + usage_key_string=quote(str(usage_key_string), safe=""), + ), + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: + if response.status_code == 200: + response_200 = Xblock.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return sync_detailed( + usage_key_string=usage_key_string, + client=client, + ).parsed + + +async def asyncio_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return ( + await asyncio_detailed( + usage_key_string=usage_key_string, + client=client, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py new file mode 100644 index 0000000..394aa51 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py @@ -0,0 +1,338 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.xblock import Xblock +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + usage_key_string: str, + *, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "put", + "url": "/v1/xblock/{usage_key_string}/".format( + usage_key_string=quote(str(usage_key_string), safe=""), + ), + } + + if isinstance(body, Xblock): + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + if isinstance(body, Xblock): + if not isinstance(body, Unset): + _kwargs["data"] = body.to_dict() + headers["Content-Type"] = "application/x-www-form-urlencoded" + if isinstance(body, Xblock): + if not isinstance(body, Unset): + _kwargs["files"] = body.to_multipart() + + headers["Content-Type"] = "multipart/form-data; boundary=+++" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: + if response.status_code == 200: + response_200 = Xblock.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return sync_detailed( + usage_key_string=usage_key_string, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Response[Xblock]: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Xblock] + """ + + kwargs = _get_kwargs( + usage_key_string=usage_key_string, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + usage_key_string: str, + *, + client: AuthenticatedClient | Client, + body: Xblock | Xblock | Xblock | Unset = UNSET, +) -> Xblock | None: + """ViewSet for xblock CRUD operations (v1 — ADR 0028). + + Router-generated URLs: + POST /api/contentstore/v1/xblock/ → create + GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve + PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update + PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update + DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy + + Query parameters (ADR 0036, GET only): + ?view=minimal Drop heavy / contextual fields from the response, + keeping only structural fields (id, display_name, + category, children, has_children, studio_url). + Default response is the full xblock payload. + + Args: + usage_key_string (str): + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + body (Xblock | Unset): A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Xblock + """ + + return ( + await asyncio_detailed( + usage_key_string=usage_key_string, + client=client, + body=body, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py new file mode 100644 index 0000000..dfbeb86 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py @@ -0,0 +1,242 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.authoring_grading_course_grading_v0 import AuthoringGradingCourseGradingV0 +from ...models.patchedauthoring_grading_course_grading_v0 import PatchedauthoringGradingCourseGradingV0 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + course_key: str, + *, + body: PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/v3/authoring_grading/{course_key}/".format( + course_key=quote(str(course_key), safe=""), + ), + } + + if isinstance(body, PatchedauthoringGradingCourseGradingV0): + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + if isinstance(body, PatchedauthoringGradingCourseGradingV0): + if not isinstance(body, Unset): + _kwargs["data"] = body.to_dict() + headers["Content-Type"] = "application/x-www-form-urlencoded" + if isinstance(body, PatchedauthoringGradingCourseGradingV0): + if not isinstance(body, Unset): + _kwargs["files"] = body.to_multipart() + + headers["Content-Type"] = "multipart/form-data; boundary=+++" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | AuthoringGradingCourseGradingV0 | None: + if response.status_code == 200: + response_200 = AuthoringGradingCourseGradingV0.from_dict(response.json()) + + return response_200 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | AuthoringGradingCourseGradingV0]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + course_key: str, + *, + client: AuthenticatedClient | Client, + body: PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | Unset = UNSET, +) -> Response[Any | AuthoringGradingCourseGradingV0]: + """Update a course's grading settings + + Partially update the grading settings for the specified course. + + Args: + course_key (str): + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | AuthoringGradingCourseGradingV0] + """ + + kwargs = _get_kwargs( + course_key=course_key, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + course_key: str, + *, + client: AuthenticatedClient | Client, + body: PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | Unset = UNSET, +) -> Any | AuthoringGradingCourseGradingV0 | None: + """Update a course's grading settings + + Partially update the grading settings for the specified course. + + Args: + course_key (str): + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | AuthoringGradingCourseGradingV0 + """ + + return sync_detailed( + course_key=course_key, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + course_key: str, + *, + client: AuthenticatedClient | Client, + body: PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | Unset = UNSET, +) -> Response[Any | AuthoringGradingCourseGradingV0]: + """Update a course's grading settings + + Partially update the grading settings for the specified course. + + Args: + course_key (str): + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | AuthoringGradingCourseGradingV0] + """ + + kwargs = _get_kwargs( + course_key=course_key, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + course_key: str, + *, + client: AuthenticatedClient | Client, + body: PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | PatchedauthoringGradingCourseGradingV0 + | Unset = UNSET, +) -> Any | AuthoringGradingCourseGradingV0 | None: + """Update a course's grading settings + + Partially update the grading settings for the specified course. + + Args: + course_key (str): + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + body (PatchedauthoringGradingCourseGradingV0 | Unset): Serializer for course grading model + data + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | AuthoringGradingCourseGradingV0 + """ + + return ( + await asyncio_detailed( + course_key=course_key, + client=client, + body=body, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_retrieve.py new file mode 100644 index 0000000..14719b4 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_retrieve.py @@ -0,0 +1,220 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_details import CourseDetails +from ...models.v3_course_details_retrieve_view import V3CourseDetailsRetrieveView +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + course_id: str, + *, + fields: str | Unset = UNSET, + view: V3CourseDetailsRetrieveView | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["fields"] = fields + + json_view: str | Unset = UNSET + if not isinstance(view, Unset): + json_view = view.value + + params["view"] = json_view + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v3/course_details/{course_id}/".format( + course_id=quote(str(course_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | CourseDetails | None: + if response.status_code == 200: + response_200 = CourseDetails.from_dict(response.json()) + + return response_200 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | CourseDetails]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + course_id: str, + *, + client: AuthenticatedClient | Client, + fields: str | Unset = UNSET, + view: V3CourseDetailsRetrieveView | Unset = UNSET, +) -> Response[Any | CourseDetails]: + """Retrieve a course's details + + Get an object containing the course details for the specified course. Supports the ADR 0036 + ``?view=minimal`` preset and ``?fields=`` explicit field selection (see the parameter descriptions + for details). + + Args: + course_id (str): + fields (str | Unset): + view (V3CourseDetailsRetrieveView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseDetails] + """ + + kwargs = _get_kwargs( + course_id=course_id, + fields=fields, + view=view, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + course_id: str, + *, + client: AuthenticatedClient | Client, + fields: str | Unset = UNSET, + view: V3CourseDetailsRetrieveView | Unset = UNSET, +) -> Any | CourseDetails | None: + """Retrieve a course's details + + Get an object containing the course details for the specified course. Supports the ADR 0036 + ``?view=minimal`` preset and ``?fields=`` explicit field selection (see the parameter descriptions + for details). + + Args: + course_id (str): + fields (str | Unset): + view (V3CourseDetailsRetrieveView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseDetails + """ + + return sync_detailed( + course_id=course_id, + client=client, + fields=fields, + view=view, + ).parsed + + +async def asyncio_detailed( + course_id: str, + *, + client: AuthenticatedClient | Client, + fields: str | Unset = UNSET, + view: V3CourseDetailsRetrieveView | Unset = UNSET, +) -> Response[Any | CourseDetails]: + """Retrieve a course's details + + Get an object containing the course details for the specified course. Supports the ADR 0036 + ``?view=minimal`` preset and ``?fields=`` explicit field selection (see the parameter descriptions + for details). + + Args: + course_id (str): + fields (str | Unset): + view (V3CourseDetailsRetrieveView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseDetails] + """ + + kwargs = _get_kwargs( + course_id=course_id, + fields=fields, + view=view, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + course_id: str, + *, + client: AuthenticatedClient | Client, + fields: str | Unset = UNSET, + view: V3CourseDetailsRetrieveView | Unset = UNSET, +) -> Any | CourseDetails | None: + """Retrieve a course's details + + Get an object containing the course details for the specified course. Supports the ADR 0036 + ``?view=minimal`` preset and ``?fields=`` explicit field selection (see the parameter descriptions + for details). + + Args: + course_id (str): + fields (str | Unset): + view (V3CourseDetailsRetrieveView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseDetails + """ + + return ( + await asyncio_detailed( + course_id=course_id, + client=client, + fields=fields, + view=view, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py new file mode 100644 index 0000000..7dddd62 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py @@ -0,0 +1,211 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_details import CourseDetails +from ...types import UNSET, Response + + +def _get_kwargs( + course_id: str, + *, + body: CourseDetails | CourseDetails | CourseDetails | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "put", + "url": "/v3/course_details/{course_id}/".format( + course_id=quote(str(course_id), safe=""), + ), + } + + if isinstance(body, CourseDetails): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + if isinstance(body, CourseDetails): + _kwargs["data"] = body.to_dict() + headers["Content-Type"] = "application/x-www-form-urlencoded" + if isinstance(body, CourseDetails): + _kwargs["files"] = body.to_multipart() + + headers["Content-Type"] = "multipart/form-data; boundary=+++" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | CourseDetails | None: + if response.status_code == 200: + response_200 = CourseDetails.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | CourseDetails]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + course_id: str, + *, + client: AuthenticatedClient | Client, + body: CourseDetails | CourseDetails | CourseDetails | Unset = UNSET, +) -> Response[Any | CourseDetails]: + """Update a course's details + + Update the details for the specified course. + + Args: + course_id (str): + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseDetails] + """ + + kwargs = _get_kwargs( + course_id=course_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + course_id: str, + *, + client: AuthenticatedClient | Client, + body: CourseDetails | CourseDetails | CourseDetails | Unset = UNSET, +) -> Any | CourseDetails | None: + """Update a course's details + + Update the details for the specified course. + + Args: + course_id (str): + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseDetails + """ + + return sync_detailed( + course_id=course_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + course_id: str, + *, + client: AuthenticatedClient | Client, + body: CourseDetails | CourseDetails | CourseDetails | Unset = UNSET, +) -> Response[Any | CourseDetails]: + """Update a course's details + + Update the details for the specified course. + + Args: + course_id (str): + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseDetails] + """ + + kwargs = _get_kwargs( + course_id=course_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + course_id: str, + *, + client: AuthenticatedClient | Client, + body: CourseDetails | CourseDetails | CourseDetails | Unset = UNSET, +) -> Any | CourseDetails | None: + """Update a course's details + + Update the details for the specified course. + + Args: + course_id (str): + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + body (CourseDetails): Serializer for course details + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseDetails + """ + + return ( + await asyncio_detailed( + course_id=course_id, + client=client, + body=body, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_courses_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_courses_retrieve.py new file mode 100644 index 0000000..d1e5fee --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_courses_retrieve.py @@ -0,0 +1,140 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_home_tab import CourseHomeTab +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v3/home/courses/", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> CourseHomeTab | None: + if response.status_code == 200: + response_200 = CourseHomeTab.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[CourseHomeTab]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[CourseHomeTab]: + """Get an object containing all courses. + + **Example Request** + + GET /api/contentstore/v3/home/courses/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CourseHomeTab] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> CourseHomeTab | None: + """Get an object containing all courses. + + **Example Request** + + GET /api/contentstore/v3/home/courses/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CourseHomeTab + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[CourseHomeTab]: + """Get an object containing all courses. + + **Example Request** + + GET /api/contentstore/v3/home/courses/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CourseHomeTab] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> CourseHomeTab | None: + """Get an object containing all courses. + + **Example Request** + + GET /api/contentstore/v3/home/courses/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CourseHomeTab + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_libraries_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_libraries_retrieve.py new file mode 100644 index 0000000..f45d7f1 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_libraries_retrieve.py @@ -0,0 +1,140 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.library_tab import LibraryTab +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v3/home/libraries/", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> LibraryTab | None: + if response.status_code == 200: + response_200 = LibraryTab.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[LibraryTab]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[LibraryTab]: + """Get an object containing all libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/libraries/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[LibraryTab] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> LibraryTab | None: + """Get an object containing all libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/libraries/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + LibraryTab + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[LibraryTab]: + """Get an object containing all libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/libraries/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[LibraryTab] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> LibraryTab | None: + """Get an object containing all libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/libraries/ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + LibraryTab + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_list.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_list.py new file mode 100644 index 0000000..6ad8065 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_list.py @@ -0,0 +1,149 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.studio_home import StudioHome +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v3/home/", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> list[StudioHome] | None: + if response.status_code == 200: + response_200 = [] + _response_200 = response.json() + for response_200_item_data in _response_200: + response_200_item = StudioHome.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[list[StudioHome]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[list[StudioHome]]: + """Get an object containing all courses and libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/ + GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[list[StudioHome]] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> list[StudioHome] | None: + """Get an object containing all courses and libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/ + GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + list[StudioHome] + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[list[StudioHome]]: + """Get an object containing all courses and libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/ + GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[list[StudioHome]] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> list[StudioHome] | None: + """Get an object containing all courses and libraries on home page. + + **Example Request** + + GET /api/contentstore/v3/home/ + GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + list[StudioHome] + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_list.py b/openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_list.py new file mode 100644 index 0000000..c60afa4 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_list.py @@ -0,0 +1,286 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_home_tab_serializer_v2 import CourseHomeTabSerializerV2 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + active_only: str | Unset = UNSET, + archived_only: str | Unset = UNSET, + order: str | Unset = UNSET, + ordering: str | Unset = UNSET, + org: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + search: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["active_only"] = active_only + + params["archived_only"] = archived_only + + params["order"] = order + + params["ordering"] = ordering + + params["org"] = org + + params["page"] = page + + params["page_size"] = page_size + + params["search"] = search + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v4/home/courses/", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | list[CourseHomeTabSerializerV2] | None: + if response.status_code == 200: + response_200 = [] + _response_200 = response.json() + for response_200_item_data in _response_200: + response_200_item = CourseHomeTabSerializerV2.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | list[CourseHomeTabSerializerV2]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + active_only: str | Unset = UNSET, + archived_only: str | Unset = UNSET, + order: str | Unset = UNSET, + ordering: str | Unset = UNSET, + org: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + search: str | Unset = UNSET, +) -> Response[Any | list[CourseHomeTabSerializerV2]]: + """List courses for the Studio home page (paginated) + + Returns a paginated list of all courses available to the logged-in user, with optional filtering and + ordering. Supersedes ``GET /api/contentstore/v2/home/courses``. + + Args: + active_only (str | Unset): + archived_only (str | Unset): + order (str | Unset): + ordering (str | Unset): + org (str | Unset): + page (str | Unset): + page_size (str | Unset): + search (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | list[CourseHomeTabSerializerV2]] + """ + + kwargs = _get_kwargs( + active_only=active_only, + archived_only=archived_only, + order=order, + ordering=ordering, + org=org, + page=page, + page_size=page_size, + search=search, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + active_only: str | Unset = UNSET, + archived_only: str | Unset = UNSET, + order: str | Unset = UNSET, + ordering: str | Unset = UNSET, + org: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + search: str | Unset = UNSET, +) -> Any | list[CourseHomeTabSerializerV2] | None: + """List courses for the Studio home page (paginated) + + Returns a paginated list of all courses available to the logged-in user, with optional filtering and + ordering. Supersedes ``GET /api/contentstore/v2/home/courses``. + + Args: + active_only (str | Unset): + archived_only (str | Unset): + order (str | Unset): + ordering (str | Unset): + org (str | Unset): + page (str | Unset): + page_size (str | Unset): + search (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | list[CourseHomeTabSerializerV2] + """ + + return sync_detailed( + client=client, + active_only=active_only, + archived_only=archived_only, + order=order, + ordering=ordering, + org=org, + page=page, + page_size=page_size, + search=search, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + active_only: str | Unset = UNSET, + archived_only: str | Unset = UNSET, + order: str | Unset = UNSET, + ordering: str | Unset = UNSET, + org: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + search: str | Unset = UNSET, +) -> Response[Any | list[CourseHomeTabSerializerV2]]: + """List courses for the Studio home page (paginated) + + Returns a paginated list of all courses available to the logged-in user, with optional filtering and + ordering. Supersedes ``GET /api/contentstore/v2/home/courses``. + + Args: + active_only (str | Unset): + archived_only (str | Unset): + order (str | Unset): + ordering (str | Unset): + org (str | Unset): + page (str | Unset): + page_size (str | Unset): + search (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | list[CourseHomeTabSerializerV2]] + """ + + kwargs = _get_kwargs( + active_only=active_only, + archived_only=archived_only, + order=order, + ordering=ordering, + org=org, + page=page, + page_size=page_size, + search=search, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + active_only: str | Unset = UNSET, + archived_only: str | Unset = UNSET, + order: str | Unset = UNSET, + ordering: str | Unset = UNSET, + org: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + search: str | Unset = UNSET, +) -> Any | list[CourseHomeTabSerializerV2] | None: + """List courses for the Studio home page (paginated) + + Returns a paginated list of all courses available to the logged-in user, with optional filtering and + ordering. Supersedes ``GET /api/contentstore/v2/home/courses``. + + Args: + active_only (str | Unset): + archived_only (str | Unset): + order (str | Unset): + ordering (str | Unset): + org (str | Unset): + page (str | Unset): + page_size (str | Unset): + search (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | list[CourseHomeTabSerializerV2] + """ + + return ( + await asyncio_detailed( + client=client, + active_only=active_only, + archived_only=archived_only, + order=order, + ordering=ordering, + org=org, + page=page, + page_size=page_size, + search=search, + ) + ).parsed diff --git a/openedx_platform_sdk/client.py b/openedx_platform_sdk/client.py new file mode 100644 index 0000000..1b7055a --- /dev/null +++ b/openedx_platform_sdk/client.py @@ -0,0 +1,268 @@ +import ssl +from typing import Any + +import httpx +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "Client": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "Client": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "Client": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "AuthenticatedClient": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "AuthenticatedClient": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/openedx_platform_sdk/errors.py b/openedx_platform_sdk/errors.py new file mode 100644 index 0000000..5f92e76 --- /dev/null +++ b/openedx_platform_sdk/errors.py @@ -0,0 +1,16 @@ +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/openedx_platform_sdk/models/__init__.py b/openedx_platform_sdk/models/__init__.py new file mode 100644 index 0000000..003193f --- /dev/null +++ b/openedx_platform_sdk/models/__init__.py @@ -0,0 +1,63 @@ +"""Contains all the data models used in inputs/outputs""" + +from .authoring_grading_course_grading_v0 import AuthoringGradingCourseGradingV0 +from .authoring_grading_graders_v0 import AuthoringGradingGradersV0 +from .course_common import CourseCommon +from .course_common_serializer_v2 import CourseCommonSerializerV2 +from .course_details import CourseDetails +from .course_home_tab import CourseHomeTab +from .course_home_tab_serializer_v2 import CourseHomeTabSerializerV2 +from .instructor_info import InstructorInfo +from .instructors import Instructors +from .library_tab import LibraryTab +from .library_view import LibraryView +from .patched_xblock import PatchedXblock +from .patched_xblock_actions_type_0 import PatchedXblockActionsType0 +from .patched_xblock_fields_type_0 import PatchedXblockFieldsType0 +from .patched_xblock_group_access_type_0 import PatchedXblockGroupAccessType0 +from .patched_xblock_metadata_type_0 import PatchedXblockMetadataType0 +from .patched_xblock_user_partition_info_type_0 import PatchedXblockUserPartitionInfoType0 +from .patchedauthoring_grading_course_grading_v0 import PatchedauthoringGradingCourseGradingV0 +from .publish_enum import PublishEnum +from .studio_home import StudioHome +from .unsucceeded_course import UnsucceededCourse +from .unsucceeded_course_serializer_v2 import UnsucceededCourseSerializerV2 +from .v3_course_details_retrieve_view import V3CourseDetailsRetrieveView +from .xblock import Xblock +from .xblock_actions_type_0 import XblockActionsType0 +from .xblock_fields_type_0 import XblockFieldsType0 +from .xblock_group_access_type_0 import XblockGroupAccessType0 +from .xblock_metadata_type_0 import XblockMetadataType0 +from .xblock_user_partition_info_type_0 import XblockUserPartitionInfoType0 + +__all__ = ( + "AuthoringGradingCourseGradingV0", + "AuthoringGradingGradersV0", + "CourseCommon", + "CourseCommonSerializerV2", + "CourseDetails", + "CourseHomeTab", + "CourseHomeTabSerializerV2", + "InstructorInfo", + "Instructors", + "LibraryTab", + "LibraryView", + "PatchedauthoringGradingCourseGradingV0", + "PatchedXblock", + "PatchedXblockActionsType0", + "PatchedXblockFieldsType0", + "PatchedXblockGroupAccessType0", + "PatchedXblockMetadataType0", + "PatchedXblockUserPartitionInfoType0", + "PublishEnum", + "StudioHome", + "UnsucceededCourse", + "UnsucceededCourseSerializerV2", + "V3CourseDetailsRetrieveView", + "Xblock", + "XblockActionsType0", + "XblockFieldsType0", + "XblockGroupAccessType0", + "XblockMetadataType0", + "XblockUserPartitionInfoType0", +) diff --git a/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py b/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py new file mode 100644 index 0000000..6c5538c --- /dev/null +++ b/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 + + +T = TypeVar("T", bound="AuthoringGradingCourseGradingV0") + + +@_attrs_define +class AuthoringGradingCourseGradingV0: + """Serializer for course grading model data + + Attributes: + graders (list[AuthoringGradingGradersV0] | None): + """ + + graders: list[AuthoringGradingGradersV0] | None + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + graders: list[dict[str, Any]] | None + if isinstance(self.graders, list): + graders = [] + for graders_type_0_item_data in self.graders: + graders_type_0_item = graders_type_0_item_data.to_dict() + graders.append(graders_type_0_item) + + else: + graders = self.graders + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "graders": graders, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 + + d = dict(src_dict) + + def _parse_graders(data: object) -> list[AuthoringGradingGradersV0] | None: + if data is None: + return data + try: + if not isinstance(data, list): + raise TypeError() + graders_type_0 = [] + _graders_type_0 = data + for graders_type_0_item_data in _graders_type_0: + graders_type_0_item = AuthoringGradingGradersV0.from_dict(graders_type_0_item_data) + + graders_type_0.append(graders_type_0_item) + + return graders_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[AuthoringGradingGradersV0] | None, data) + + graders = _parse_graders(d.pop("graders")) + + authoring_grading_course_grading_v0 = cls( + graders=graders, + ) + + authoring_grading_course_grading_v0.additional_properties = d + return authoring_grading_course_grading_v0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/authoring_grading_graders_v0.py b/openedx_platform_sdk/models/authoring_grading_graders_v0.py new file mode 100644 index 0000000..c949b5c --- /dev/null +++ b/openedx_platform_sdk/models/authoring_grading_graders_v0.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AuthoringGradingGradersV0") + + +@_attrs_define +class AuthoringGradingGradersV0: + """Serializer for graders + + Attributes: + type_ (str): + min_count (int): + drop_count (int): + weight (int): + id (int): + short_label (None | str | Unset): + """ + + type_: str + min_count: int + drop_count: int + weight: int + id: int + short_label: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + min_count = self.min_count + + drop_count = self.drop_count + + weight = self.weight + + id = self.id + + short_label: None | str | Unset + if isinstance(self.short_label, Unset): + short_label = UNSET + else: + short_label = self.short_label + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "min_count": min_count, + "drop_count": drop_count, + "weight": weight, + "id": id, + } + ) + if short_label is not UNSET: + field_dict["short_label"] = short_label + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + type_ = d.pop("type") + + min_count = d.pop("min_count") + + drop_count = d.pop("drop_count") + + weight = d.pop("weight") + + id = d.pop("id") + + def _parse_short_label(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + short_label = _parse_short_label(d.pop("short_label", UNSET)) + + authoring_grading_graders_v0 = cls( + type_=type_, + min_count=min_count, + drop_count=drop_count, + weight=weight, + id=id, + short_label=short_label, + ) + + authoring_grading_graders_v0.additional_properties = d + return authoring_grading_graders_v0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_common.py b/openedx_platform_sdk/models/course_common.py new file mode 100644 index 0000000..8c4ea5b --- /dev/null +++ b/openedx_platform_sdk/models/course_common.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="CourseCommon") + + +@_attrs_define +class CourseCommon: + """Serializer for course renders + + Attributes: + course_key (str): + display_name (str): + lms_link (str): + number (str): + org (str): + rerun_link (str): + run (str): + url (str): + """ + + course_key: str + display_name: str + lms_link: str + number: str + org: str + rerun_link: str + run: str + url: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + course_key = self.course_key + + display_name = self.display_name + + lms_link = self.lms_link + + number = self.number + + org = self.org + + rerun_link = self.rerun_link + + run = self.run + + url = self.url + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "course_key": course_key, + "display_name": display_name, + "lms_link": lms_link, + "number": number, + "org": org, + "rerun_link": rerun_link, + "run": run, + "url": url, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + course_key = d.pop("course_key") + + display_name = d.pop("display_name") + + lms_link = d.pop("lms_link") + + number = d.pop("number") + + org = d.pop("org") + + rerun_link = d.pop("rerun_link") + + run = d.pop("run") + + url = d.pop("url") + + course_common = cls( + course_key=course_key, + display_name=display_name, + lms_link=lms_link, + number=number, + org=org, + rerun_link=rerun_link, + run=run, + url=url, + ) + + course_common.additional_properties = d + return course_common + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_common_serializer_v2.py b/openedx_platform_sdk/models/course_common_serializer_v2.py new file mode 100644 index 0000000..5f31716 --- /dev/null +++ b/openedx_platform_sdk/models/course_common_serializer_v2.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="CourseCommonSerializerV2") + + +@_attrs_define +class CourseCommonSerializerV2: + """Serializer for course common fields V2. + + Attributes: + course_key (str): + display_name (str): + lms_link (str): + cms_link (str): + number (str): + org (str): + rerun_link (str): + run (str): + url (str): + is_active (str): + """ + + course_key: str + display_name: str + lms_link: str + cms_link: str + number: str + org: str + rerun_link: str + run: str + url: str + is_active: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + course_key = self.course_key + + display_name = self.display_name + + lms_link = self.lms_link + + cms_link = self.cms_link + + number = self.number + + org = self.org + + rerun_link = self.rerun_link + + run = self.run + + url = self.url + + is_active = self.is_active + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "course_key": course_key, + "display_name": display_name, + "lms_link": lms_link, + "cms_link": cms_link, + "number": number, + "org": org, + "rerun_link": rerun_link, + "run": run, + "url": url, + "is_active": is_active, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + course_key = d.pop("course_key") + + display_name = d.pop("display_name") + + lms_link = d.pop("lms_link") + + cms_link = d.pop("cms_link") + + number = d.pop("number") + + org = d.pop("org") + + rerun_link = d.pop("rerun_link") + + run = d.pop("run") + + url = d.pop("url") + + is_active = d.pop("is_active") + + course_common_serializer_v2 = cls( + course_key=course_key, + display_name=display_name, + lms_link=lms_link, + cms_link=cms_link, + number=number, + org=org, + rerun_link=rerun_link, + run=run, + url=url, + is_active=is_active, + ) + + course_common_serializer_v2.additional_properties = d + return course_common_serializer_v2 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_details.py b/openedx_platform_sdk/models/course_details.py new file mode 100644 index 0000000..7d0ab72 --- /dev/null +++ b/openedx_platform_sdk/models/course_details.py @@ -0,0 +1,576 @@ +from __future__ import annotations + +import datetime +import json +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types + +if TYPE_CHECKING: + from ..models.instructors import Instructors + + +T = TypeVar("T", bound="CourseDetails") + + +@_attrs_define +class CourseDetails: + """Serializer for course details + + Attributes: + about_sidebar_html (None | str): + banner_image_name (str): + banner_image_asset_path (str): + certificate_available_date (datetime.datetime): + certificates_display_behavior (None | str): + course_id (str): + course_image_asset_path (str): + course_image_name (str): + description (str): + duration (str): + effort (None | str): + end_date (datetime.datetime | None): + enrollment_end (datetime.datetime | None): + enrollment_start (datetime.datetime | None): + entrance_exam_enabled (str): + entrance_exam_id (str): + entrance_exam_minimum_score_pct (str): + instructor_info (Instructors): Serializer for instructors + intro_video (None | str): + language (None | str): + learning_info (list[str]): + license_ (None | str): + org (str): + overview (str): + pre_requisite_courses (list[str]): + run (str): + self_paced (bool): + has_changes (bool): + short_description (str): + start_date (datetime.datetime): + subtitle (str): + syllabus (None | str): + title (str): + video_thumbnail_image_asset_path (str): + video_thumbnail_image_name (str): + """ + + about_sidebar_html: None | str + banner_image_name: str + banner_image_asset_path: str + certificate_available_date: datetime.datetime + certificates_display_behavior: None | str + course_id: str + course_image_asset_path: str + course_image_name: str + description: str + duration: str + effort: None | str + end_date: datetime.datetime | None + enrollment_end: datetime.datetime | None + enrollment_start: datetime.datetime | None + entrance_exam_enabled: str + entrance_exam_id: str + entrance_exam_minimum_score_pct: str + instructor_info: Instructors + intro_video: None | str + language: None | str + learning_info: list[str] + license_: None | str + org: str + overview: str + pre_requisite_courses: list[str] + run: str + self_paced: bool + has_changes: bool + short_description: str + start_date: datetime.datetime + subtitle: str + syllabus: None | str + title: str + video_thumbnail_image_asset_path: str + video_thumbnail_image_name: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + about_sidebar_html: None | str + about_sidebar_html = self.about_sidebar_html + + banner_image_name = self.banner_image_name + + banner_image_asset_path = self.banner_image_asset_path + + certificate_available_date = self.certificate_available_date.isoformat() + + certificates_display_behavior: None | str + certificates_display_behavior = self.certificates_display_behavior + + course_id = self.course_id + + course_image_asset_path = self.course_image_asset_path + + course_image_name = self.course_image_name + + description = self.description + + duration = self.duration + + effort: None | str + effort = self.effort + + end_date: None | str + if isinstance(self.end_date, datetime.datetime): + end_date = self.end_date.isoformat() + else: + end_date = self.end_date + + enrollment_end: None | str + if isinstance(self.enrollment_end, datetime.datetime): + enrollment_end = self.enrollment_end.isoformat() + else: + enrollment_end = self.enrollment_end + + enrollment_start: None | str + if isinstance(self.enrollment_start, datetime.datetime): + enrollment_start = self.enrollment_start.isoformat() + else: + enrollment_start = self.enrollment_start + + entrance_exam_enabled = self.entrance_exam_enabled + + entrance_exam_id = self.entrance_exam_id + + entrance_exam_minimum_score_pct = self.entrance_exam_minimum_score_pct + + instructor_info = self.instructor_info.to_dict() + + intro_video: None | str + intro_video = self.intro_video + + language: None | str + language = self.language + + learning_info = self.learning_info + + license_: None | str + license_ = self.license_ + + org = self.org + + overview = self.overview + + pre_requisite_courses = self.pre_requisite_courses + + run = self.run + + self_paced = self.self_paced + + has_changes = self.has_changes + + short_description = self.short_description + + start_date = self.start_date.isoformat() + + subtitle = self.subtitle + + syllabus: None | str + syllabus = self.syllabus + + title = self.title + + video_thumbnail_image_asset_path = self.video_thumbnail_image_asset_path + + video_thumbnail_image_name = self.video_thumbnail_image_name + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "about_sidebar_html": about_sidebar_html, + "banner_image_name": banner_image_name, + "banner_image_asset_path": banner_image_asset_path, + "certificate_available_date": certificate_available_date, + "certificates_display_behavior": certificates_display_behavior, + "course_id": course_id, + "course_image_asset_path": course_image_asset_path, + "course_image_name": course_image_name, + "description": description, + "duration": duration, + "effort": effort, + "end_date": end_date, + "enrollment_end": enrollment_end, + "enrollment_start": enrollment_start, + "entrance_exam_enabled": entrance_exam_enabled, + "entrance_exam_id": entrance_exam_id, + "entrance_exam_minimum_score_pct": entrance_exam_minimum_score_pct, + "instructor_info": instructor_info, + "intro_video": intro_video, + "language": language, + "learning_info": learning_info, + "license": license_, + "org": org, + "overview": overview, + "pre_requisite_courses": pre_requisite_courses, + "run": run, + "self_paced": self_paced, + "has_changes": has_changes, + "short_description": short_description, + "start_date": start_date, + "subtitle": subtitle, + "syllabus": syllabus, + "title": title, + "video_thumbnail_image_asset_path": video_thumbnail_image_asset_path, + "video_thumbnail_image_name": video_thumbnail_image_name, + } + ) + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + if isinstance(self.about_sidebar_html, str): + files.append(("about_sidebar_html", (None, str(self.about_sidebar_html).encode(), "text/plain"))) + else: + files.append(("about_sidebar_html", (None, str(self.about_sidebar_html).encode(), "text/plain"))) + + files.append(("banner_image_name", (None, str(self.banner_image_name).encode(), "text/plain"))) + + files.append(("banner_image_asset_path", (None, str(self.banner_image_asset_path).encode(), "text/plain"))) + + files.append( + ("certificate_available_date", (None, self.certificate_available_date.isoformat().encode(), "text/plain")) + ) + + if isinstance(self.certificates_display_behavior, str): + files.append( + ( + "certificates_display_behavior", + (None, str(self.certificates_display_behavior).encode(), "text/plain"), + ) + ) + else: + files.append( + ( + "certificates_display_behavior", + (None, str(self.certificates_display_behavior).encode(), "text/plain"), + ) + ) + + files.append(("course_id", (None, str(self.course_id).encode(), "text/plain"))) + + files.append(("course_image_asset_path", (None, str(self.course_image_asset_path).encode(), "text/plain"))) + + files.append(("course_image_name", (None, str(self.course_image_name).encode(), "text/plain"))) + + files.append(("description", (None, str(self.description).encode(), "text/plain"))) + + files.append(("duration", (None, str(self.duration).encode(), "text/plain"))) + + if isinstance(self.effort, str): + files.append(("effort", (None, str(self.effort).encode(), "text/plain"))) + else: + files.append(("effort", (None, str(self.effort).encode(), "text/plain"))) + + if isinstance(self.end_date, datetime.datetime): + files.append(("end_date", (None, self.end_date.isoformat().encode(), "text/plain"))) + else: + files.append(("end_date", (None, str(self.end_date).encode(), "text/plain"))) + + if isinstance(self.enrollment_end, datetime.datetime): + files.append(("enrollment_end", (None, self.enrollment_end.isoformat().encode(), "text/plain"))) + else: + files.append(("enrollment_end", (None, str(self.enrollment_end).encode(), "text/plain"))) + + if isinstance(self.enrollment_start, datetime.datetime): + files.append(("enrollment_start", (None, self.enrollment_start.isoformat().encode(), "text/plain"))) + else: + files.append(("enrollment_start", (None, str(self.enrollment_start).encode(), "text/plain"))) + + files.append(("entrance_exam_enabled", (None, str(self.entrance_exam_enabled).encode(), "text/plain"))) + + files.append(("entrance_exam_id", (None, str(self.entrance_exam_id).encode(), "text/plain"))) + + files.append( + ( + "entrance_exam_minimum_score_pct", + (None, str(self.entrance_exam_minimum_score_pct).encode(), "text/plain"), + ) + ) + + files.append( + ("instructor_info", (None, json.dumps(self.instructor_info.to_dict()).encode(), "application/json")) + ) + + if isinstance(self.intro_video, str): + files.append(("intro_video", (None, str(self.intro_video).encode(), "text/plain"))) + else: + files.append(("intro_video", (None, str(self.intro_video).encode(), "text/plain"))) + + if isinstance(self.language, str): + files.append(("language", (None, str(self.language).encode(), "text/plain"))) + else: + files.append(("language", (None, str(self.language).encode(), "text/plain"))) + + for learning_info_item_element in self.learning_info: + files.append(("learning_info", (None, str(learning_info_item_element).encode(), "text/plain"))) + + if isinstance(self.license_, str): + files.append(("license", (None, str(self.license_).encode(), "text/plain"))) + else: + files.append(("license", (None, str(self.license_).encode(), "text/plain"))) + + files.append(("org", (None, str(self.org).encode(), "text/plain"))) + + files.append(("overview", (None, str(self.overview).encode(), "text/plain"))) + + for pre_requisite_courses_item_element in self.pre_requisite_courses: + files.append( + ("pre_requisite_courses", (None, str(pre_requisite_courses_item_element).encode(), "text/plain")) + ) + + files.append(("run", (None, str(self.run).encode(), "text/plain"))) + + files.append(("self_paced", (None, str(self.self_paced).encode(), "text/plain"))) + + files.append(("has_changes", (None, str(self.has_changes).encode(), "text/plain"))) + + files.append(("short_description", (None, str(self.short_description).encode(), "text/plain"))) + + files.append(("start_date", (None, self.start_date.isoformat().encode(), "text/plain"))) + + files.append(("subtitle", (None, str(self.subtitle).encode(), "text/plain"))) + + if isinstance(self.syllabus, str): + files.append(("syllabus", (None, str(self.syllabus).encode(), "text/plain"))) + else: + files.append(("syllabus", (None, str(self.syllabus).encode(), "text/plain"))) + + files.append(("title", (None, str(self.title).encode(), "text/plain"))) + + files.append( + ( + "video_thumbnail_image_asset_path", + (None, str(self.video_thumbnail_image_asset_path).encode(), "text/plain"), + ) + ) + + files.append( + ("video_thumbnail_image_name", (None, str(self.video_thumbnail_image_name).encode(), "text/plain")) + ) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.instructors import Instructors + + d = dict(src_dict) + + def _parse_about_sidebar_html(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + about_sidebar_html = _parse_about_sidebar_html(d.pop("about_sidebar_html")) + + banner_image_name = d.pop("banner_image_name") + + banner_image_asset_path = d.pop("banner_image_asset_path") + + certificate_available_date = datetime.datetime.fromisoformat(d.pop("certificate_available_date")) + + def _parse_certificates_display_behavior(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + certificates_display_behavior = _parse_certificates_display_behavior(d.pop("certificates_display_behavior")) + + course_id = d.pop("course_id") + + course_image_asset_path = d.pop("course_image_asset_path") + + course_image_name = d.pop("course_image_name") + + description = d.pop("description") + + duration = d.pop("duration") + + def _parse_effort(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + effort = _parse_effort(d.pop("effort")) + + def _parse_end_date(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + end_date_type_0 = datetime.datetime.fromisoformat(data) + + return end_date_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + end_date = _parse_end_date(d.pop("end_date")) + + def _parse_enrollment_end(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + enrollment_end_type_0 = datetime.datetime.fromisoformat(data) + + return enrollment_end_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + enrollment_end = _parse_enrollment_end(d.pop("enrollment_end")) + + def _parse_enrollment_start(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + enrollment_start_type_0 = datetime.datetime.fromisoformat(data) + + return enrollment_start_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + enrollment_start = _parse_enrollment_start(d.pop("enrollment_start")) + + entrance_exam_enabled = d.pop("entrance_exam_enabled") + + entrance_exam_id = d.pop("entrance_exam_id") + + entrance_exam_minimum_score_pct = d.pop("entrance_exam_minimum_score_pct") + + instructor_info = Instructors.from_dict(d.pop("instructor_info")) + + def _parse_intro_video(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + intro_video = _parse_intro_video(d.pop("intro_video")) + + def _parse_language(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + language = _parse_language(d.pop("language")) + + learning_info = cast(list[str], d.pop("learning_info")) + + def _parse_license_(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + license_ = _parse_license_(d.pop("license")) + + org = d.pop("org") + + overview = d.pop("overview") + + pre_requisite_courses = cast(list[str], d.pop("pre_requisite_courses")) + + run = d.pop("run") + + self_paced = d.pop("self_paced") + + has_changes = d.pop("has_changes") + + short_description = d.pop("short_description") + + start_date = datetime.datetime.fromisoformat(d.pop("start_date")) + + subtitle = d.pop("subtitle") + + def _parse_syllabus(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + syllabus = _parse_syllabus(d.pop("syllabus")) + + title = d.pop("title") + + video_thumbnail_image_asset_path = d.pop("video_thumbnail_image_asset_path") + + video_thumbnail_image_name = d.pop("video_thumbnail_image_name") + + course_details = cls( + about_sidebar_html=about_sidebar_html, + banner_image_name=banner_image_name, + banner_image_asset_path=banner_image_asset_path, + certificate_available_date=certificate_available_date, + certificates_display_behavior=certificates_display_behavior, + course_id=course_id, + course_image_asset_path=course_image_asset_path, + course_image_name=course_image_name, + description=description, + duration=duration, + effort=effort, + end_date=end_date, + enrollment_end=enrollment_end, + enrollment_start=enrollment_start, + entrance_exam_enabled=entrance_exam_enabled, + entrance_exam_id=entrance_exam_id, + entrance_exam_minimum_score_pct=entrance_exam_minimum_score_pct, + instructor_info=instructor_info, + intro_video=intro_video, + language=language, + learning_info=learning_info, + license_=license_, + org=org, + overview=overview, + pre_requisite_courses=pre_requisite_courses, + run=run, + self_paced=self_paced, + has_changes=has_changes, + short_description=short_description, + start_date=start_date, + subtitle=subtitle, + syllabus=syllabus, + title=title, + video_thumbnail_image_asset_path=video_thumbnail_image_asset_path, + video_thumbnail_image_name=video_thumbnail_image_name, + ) + + course_details.additional_properties = d + return course_details + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_home_tab.py b/openedx_platform_sdk/models/course_home_tab.py new file mode 100644 index 0000000..aab8a45 --- /dev/null +++ b/openedx_platform_sdk/models/course_home_tab.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.course_common import CourseCommon + from ..models.unsucceeded_course import UnsucceededCourse + + +T = TypeVar("T", bound="CourseHomeTab") + + +@_attrs_define +class CourseHomeTab: + """ + Attributes: + archived_courses (list[CourseCommon] | Unset): + courses (list[CourseCommon] | Unset): + in_process_course_actions (list[UnsucceededCourse] | None | Unset): + """ + + archived_courses: list[CourseCommon] | Unset = UNSET + courses: list[CourseCommon] | Unset = UNSET + in_process_course_actions: list[UnsucceededCourse] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + archived_courses: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.archived_courses, Unset): + archived_courses = [] + for archived_courses_item_data in self.archived_courses: + archived_courses_item = archived_courses_item_data.to_dict() + archived_courses.append(archived_courses_item) + + courses: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.courses, Unset): + courses = [] + for courses_item_data in self.courses: + courses_item = courses_item_data.to_dict() + courses.append(courses_item) + + in_process_course_actions: list[dict[str, Any]] | None | Unset + if isinstance(self.in_process_course_actions, Unset): + in_process_course_actions = UNSET + elif isinstance(self.in_process_course_actions, list): + in_process_course_actions = [] + for in_process_course_actions_type_0_item_data in self.in_process_course_actions: + in_process_course_actions_type_0_item = in_process_course_actions_type_0_item_data.to_dict() + in_process_course_actions.append(in_process_course_actions_type_0_item) + + else: + in_process_course_actions = self.in_process_course_actions + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if archived_courses is not UNSET: + field_dict["archived_courses"] = archived_courses + if courses is not UNSET: + field_dict["courses"] = courses + if in_process_course_actions is not UNSET: + field_dict["in_process_course_actions"] = in_process_course_actions + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.course_common import CourseCommon + from ..models.unsucceeded_course import UnsucceededCourse + + d = dict(src_dict) + _archived_courses = d.pop("archived_courses", UNSET) + archived_courses: list[CourseCommon] | Unset = UNSET + if _archived_courses is not UNSET: + archived_courses = [] + for archived_courses_item_data in _archived_courses: + archived_courses_item = CourseCommon.from_dict(archived_courses_item_data) + + archived_courses.append(archived_courses_item) + + _courses = d.pop("courses", UNSET) + courses: list[CourseCommon] | Unset = UNSET + if _courses is not UNSET: + courses = [] + for courses_item_data in _courses: + courses_item = CourseCommon.from_dict(courses_item_data) + + courses.append(courses_item) + + def _parse_in_process_course_actions(data: object) -> list[UnsucceededCourse] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + in_process_course_actions_type_0 = [] + _in_process_course_actions_type_0 = data + for in_process_course_actions_type_0_item_data in _in_process_course_actions_type_0: + in_process_course_actions_type_0_item = UnsucceededCourse.from_dict( + in_process_course_actions_type_0_item_data + ) + + in_process_course_actions_type_0.append(in_process_course_actions_type_0_item) + + return in_process_course_actions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[UnsucceededCourse] | None | Unset, data) + + in_process_course_actions = _parse_in_process_course_actions(d.pop("in_process_course_actions", UNSET)) + + course_home_tab = cls( + archived_courses=archived_courses, + courses=courses, + in_process_course_actions=in_process_course_actions, + ) + + course_home_tab.additional_properties = d + return course_home_tab + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_home_tab_serializer_v2.py b/openedx_platform_sdk/models/course_home_tab_serializer_v2.py new file mode 100644 index 0000000..bf4b31b --- /dev/null +++ b/openedx_platform_sdk/models/course_home_tab_serializer_v2.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.course_common_serializer_v2 import CourseCommonSerializerV2 + from ..models.unsucceeded_course_serializer_v2 import UnsucceededCourseSerializerV2 + + +T = TypeVar("T", bound="CourseHomeTabSerializerV2") + + +@_attrs_define +class CourseHomeTabSerializerV2: + """Serializer for course home tab V2 with unsucceeded courses and in process course actions. + + Attributes: + courses (list[CourseCommonSerializerV2] | Unset): + in_process_course_actions (list[UnsucceededCourseSerializerV2] | None | Unset): + """ + + courses: list[CourseCommonSerializerV2] | Unset = UNSET + in_process_course_actions: list[UnsucceededCourseSerializerV2] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + courses: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.courses, Unset): + courses = [] + for courses_item_data in self.courses: + courses_item = courses_item_data.to_dict() + courses.append(courses_item) + + in_process_course_actions: list[dict[str, Any]] | None | Unset + if isinstance(self.in_process_course_actions, Unset): + in_process_course_actions = UNSET + elif isinstance(self.in_process_course_actions, list): + in_process_course_actions = [] + for in_process_course_actions_type_0_item_data in self.in_process_course_actions: + in_process_course_actions_type_0_item = in_process_course_actions_type_0_item_data.to_dict() + in_process_course_actions.append(in_process_course_actions_type_0_item) + + else: + in_process_course_actions = self.in_process_course_actions + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if courses is not UNSET: + field_dict["courses"] = courses + if in_process_course_actions is not UNSET: + field_dict["in_process_course_actions"] = in_process_course_actions + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.course_common_serializer_v2 import CourseCommonSerializerV2 + from ..models.unsucceeded_course_serializer_v2 import UnsucceededCourseSerializerV2 + + d = dict(src_dict) + _courses = d.pop("courses", UNSET) + courses: list[CourseCommonSerializerV2] | Unset = UNSET + if _courses is not UNSET: + courses = [] + for courses_item_data in _courses: + courses_item = CourseCommonSerializerV2.from_dict(courses_item_data) + + courses.append(courses_item) + + def _parse_in_process_course_actions(data: object) -> list[UnsucceededCourseSerializerV2] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + in_process_course_actions_type_0 = [] + _in_process_course_actions_type_0 = data + for in_process_course_actions_type_0_item_data in _in_process_course_actions_type_0: + in_process_course_actions_type_0_item = UnsucceededCourseSerializerV2.from_dict( + in_process_course_actions_type_0_item_data + ) + + in_process_course_actions_type_0.append(in_process_course_actions_type_0_item) + + return in_process_course_actions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[UnsucceededCourseSerializerV2] | None | Unset, data) + + in_process_course_actions = _parse_in_process_course_actions(d.pop("in_process_course_actions", UNSET)) + + course_home_tab_serializer_v2 = cls( + courses=courses, + in_process_course_actions=in_process_course_actions, + ) + + course_home_tab_serializer_v2.additional_properties = d + return course_home_tab_serializer_v2 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/instructor_info.py b/openedx_platform_sdk/models/instructor_info.py new file mode 100644 index 0000000..ac7fec0 --- /dev/null +++ b/openedx_platform_sdk/models/instructor_info.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InstructorInfo") + + +@_attrs_define +class InstructorInfo: + """Serializer for instructor info + + Attributes: + title (str): + name (str | Unset): + organization (str | Unset): + image (str | Unset): + bio (str | Unset): + """ + + title: str + name: str | Unset = UNSET + organization: str | Unset = UNSET + image: str | Unset = UNSET + bio: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + title = self.title + + name = self.name + + organization = self.organization + + image = self.image + + bio = self.bio + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "title": title, + } + ) + if name is not UNSET: + field_dict["name"] = name + if organization is not UNSET: + field_dict["organization"] = organization + if image is not UNSET: + field_dict["image"] = image + if bio is not UNSET: + field_dict["bio"] = bio + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + title = d.pop("title") + + name = d.pop("name", UNSET) + + organization = d.pop("organization", UNSET) + + image = d.pop("image", UNSET) + + bio = d.pop("bio", UNSET) + + instructor_info = cls( + title=title, + name=name, + organization=organization, + image=image, + bio=bio, + ) + + instructor_info.additional_properties = d + return instructor_info + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/instructors.py b/openedx_platform_sdk/models/instructors.py new file mode 100644 index 0000000..60758f5 --- /dev/null +++ b/openedx_platform_sdk/models/instructors.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.instructor_info import InstructorInfo + + +T = TypeVar("T", bound="Instructors") + + +@_attrs_define +class Instructors: + """Serializer for instructors + + Attributes: + instructors (list[InstructorInfo] | None | Unset): + """ + + instructors: list[InstructorInfo] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + instructors: list[dict[str, Any]] | None | Unset + if isinstance(self.instructors, Unset): + instructors = UNSET + elif isinstance(self.instructors, list): + instructors = [] + for instructors_type_0_item_data in self.instructors: + instructors_type_0_item = instructors_type_0_item_data.to_dict() + instructors.append(instructors_type_0_item) + + else: + instructors = self.instructors + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if instructors is not UNSET: + field_dict["instructors"] = instructors + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.instructor_info import InstructorInfo + + d = dict(src_dict) + + def _parse_instructors(data: object) -> list[InstructorInfo] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + instructors_type_0 = [] + _instructors_type_0 = data + for instructors_type_0_item_data in _instructors_type_0: + instructors_type_0_item = InstructorInfo.from_dict(instructors_type_0_item_data) + + instructors_type_0.append(instructors_type_0_item) + + return instructors_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[InstructorInfo] | None | Unset, data) + + instructors = _parse_instructors(d.pop("instructors", UNSET)) + + instructors = cls( + instructors=instructors, + ) + + instructors.additional_properties = d + return instructors + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/library_tab.py b/openedx_platform_sdk/models/library_tab.py new file mode 100644 index 0000000..e044a7e --- /dev/null +++ b/openedx_platform_sdk/models/library_tab.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.library_view import LibraryView + + +T = TypeVar("T", bound="LibraryTab") + + +@_attrs_define +class LibraryTab: + """ + Attributes: + libraries (list[LibraryView] | None | Unset): + """ + + libraries: list[LibraryView] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + libraries: list[dict[str, Any]] | None | Unset + if isinstance(self.libraries, Unset): + libraries = UNSET + elif isinstance(self.libraries, list): + libraries = [] + for libraries_type_0_item_data in self.libraries: + libraries_type_0_item = libraries_type_0_item_data.to_dict() + libraries.append(libraries_type_0_item) + + else: + libraries = self.libraries + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if libraries is not UNSET: + field_dict["libraries"] = libraries + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.library_view import LibraryView + + d = dict(src_dict) + + def _parse_libraries(data: object) -> list[LibraryView] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + libraries_type_0 = [] + _libraries_type_0 = data + for libraries_type_0_item_data in _libraries_type_0: + libraries_type_0_item = LibraryView.from_dict(libraries_type_0_item_data) + + libraries_type_0.append(libraries_type_0_item) + + return libraries_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[LibraryView] | None | Unset, data) + + libraries = _parse_libraries(d.pop("libraries", UNSET)) + + library_tab = cls( + libraries=libraries, + ) + + library_tab.additional_properties = d + return library_tab + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/library_view.py b/openedx_platform_sdk/models/library_view.py new file mode 100644 index 0000000..17dc563 --- /dev/null +++ b/openedx_platform_sdk/models/library_view.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="LibraryView") + + +@_attrs_define +class LibraryView: + """Serializer for library view + + Attributes: + display_name (str): + library_key (str): + url (str): + org (str): + number (str): + can_edit (bool): + is_migrated (bool): + migrated_to_title (str | Unset): + migrated_to_key (str | Unset): + migrated_to_collection_key (str | Unset): + migrated_to_collection_title (str | Unset): + """ + + display_name: str + library_key: str + url: str + org: str + number: str + can_edit: bool + is_migrated: bool + migrated_to_title: str | Unset = UNSET + migrated_to_key: str | Unset = UNSET + migrated_to_collection_key: str | Unset = UNSET + migrated_to_collection_title: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + display_name = self.display_name + + library_key = self.library_key + + url = self.url + + org = self.org + + number = self.number + + can_edit = self.can_edit + + is_migrated = self.is_migrated + + migrated_to_title = self.migrated_to_title + + migrated_to_key = self.migrated_to_key + + migrated_to_collection_key = self.migrated_to_collection_key + + migrated_to_collection_title = self.migrated_to_collection_title + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "display_name": display_name, + "library_key": library_key, + "url": url, + "org": org, + "number": number, + "can_edit": can_edit, + "is_migrated": is_migrated, + } + ) + if migrated_to_title is not UNSET: + field_dict["migrated_to_title"] = migrated_to_title + if migrated_to_key is not UNSET: + field_dict["migrated_to_key"] = migrated_to_key + if migrated_to_collection_key is not UNSET: + field_dict["migrated_to_collection_key"] = migrated_to_collection_key + if migrated_to_collection_title is not UNSET: + field_dict["migrated_to_collection_title"] = migrated_to_collection_title + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + display_name = d.pop("display_name") + + library_key = d.pop("library_key") + + url = d.pop("url") + + org = d.pop("org") + + number = d.pop("number") + + can_edit = d.pop("can_edit") + + is_migrated = d.pop("is_migrated") + + migrated_to_title = d.pop("migrated_to_title", UNSET) + + migrated_to_key = d.pop("migrated_to_key", UNSET) + + migrated_to_collection_key = d.pop("migrated_to_collection_key", UNSET) + + migrated_to_collection_title = d.pop("migrated_to_collection_title", UNSET) + + library_view = cls( + display_name=display_name, + library_key=library_key, + url=url, + org=org, + number=number, + can_edit=can_edit, + is_migrated=is_migrated, + migrated_to_title=migrated_to_title, + migrated_to_key=migrated_to_key, + migrated_to_collection_key=migrated_to_collection_key, + migrated_to_collection_title=migrated_to_collection_title, + ) + + library_view.additional_properties = d + return library_view + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patched_xblock.py b/openedx_platform_sdk/models/patched_xblock.py new file mode 100644 index 0000000..2f1a522 --- /dev/null +++ b/openedx_platform_sdk/models/patched_xblock.py @@ -0,0 +1,1397 @@ +from __future__ import annotations + +import json +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..models.publish_enum import PublishEnum +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.patched_xblock_actions_type_0 import PatchedXblockActionsType0 + from ..models.patched_xblock_fields_type_0 import PatchedXblockFieldsType0 + from ..models.patched_xblock_group_access_type_0 import PatchedXblockGroupAccessType0 + from ..models.patched_xblock_metadata_type_0 import PatchedXblockMetadataType0 + from ..models.patched_xblock_user_partition_info_type_0 import PatchedXblockUserPartitionInfoType0 + + +T = TypeVar("T", bound="PatchedXblock") + + +@_attrs_define +class PatchedXblock: + """A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Attributes: + id (None | str | Unset): + parent_locator (None | str | Unset): + display_name (None | str | Unset): + category (None | str | Unset): + data (None | str | Unset): + metadata (None | PatchedXblockMetadataType0 | Unset): + has_changes (bool | None | Unset): + children (list[Any] | None | Unset): + fields (None | PatchedXblockFieldsType0 | Unset): + has_children (bool | None | Unset): + video_sharing_enabled (bool | None | Unset): + video_sharing_options (None | str | Unset): + video_sharing_doc_url (None | str | Unset): + edited_on (None | str | Unset): + published (bool | None | Unset): + published_on (Any | Unset): + studio_url (None | str | Unset): + released_to_students (bool | None | Unset): + release_date (Any | Unset): + nullout (Any | Unset): + grader_type (Any | Unset): + visibility_state (None | str | Unset): + has_explicit_staff_lock (bool | None | Unset): + start (None | str | Unset): + graded (bool | None | Unset): + due_date (None | str | Unset): + due (Any | Unset): + relative_weeks_due (Any | Unset): + format_ (Any | Unset): + course_graders (list[Any] | None | Unset): + actions (None | PatchedXblockActionsType0 | Unset): + explanatory_message (None | str | Unset): + group_access (None | PatchedXblockGroupAccessType0 | Unset): + user_partitions (list[Any] | None | Unset): + show_correctness (None | str | Unset): + discussion_enabled (bool | None | Unset): + ancestor_has_staff_lock (bool | None | Unset): + user_partition_info (None | PatchedXblockUserPartitionInfoType0 | Unset): + summary_configuration_enabled (Any | Unset): + is_prereq (bool | None | Unset): + prereq_usage_key (None | str | Unset): + prereq_min_score (int | None | Unset): + prereq_min_completion (int | None | Unset): + publish (None | PublishEnum | Unset): + duplicate_source_locator (None | str | Unset): + move_source_locator (None | str | Unset): + target_index (int | None | Unset): + boilerplate (Any | Unset): + staged_content (None | str | Unset): + hide_from_toc (bool | None | Unset): + """ + + id: None | str | Unset = UNSET + parent_locator: None | str | Unset = UNSET + display_name: None | str | Unset = UNSET + category: None | str | Unset = UNSET + data: None | str | Unset = UNSET + metadata: None | PatchedXblockMetadataType0 | Unset = UNSET + has_changes: bool | None | Unset = UNSET + children: list[Any] | None | Unset = UNSET + fields: None | PatchedXblockFieldsType0 | Unset = UNSET + has_children: bool | None | Unset = UNSET + video_sharing_enabled: bool | None | Unset = UNSET + video_sharing_options: None | str | Unset = UNSET + video_sharing_doc_url: None | str | Unset = UNSET + edited_on: None | str | Unset = UNSET + published: bool | None | Unset = UNSET + published_on: Any | Unset = UNSET + studio_url: None | str | Unset = UNSET + released_to_students: bool | None | Unset = UNSET + release_date: Any | Unset = UNSET + nullout: Any | Unset = UNSET + grader_type: Any | Unset = UNSET + visibility_state: None | str | Unset = UNSET + has_explicit_staff_lock: bool | None | Unset = UNSET + start: None | str | Unset = UNSET + graded: bool | None | Unset = UNSET + due_date: None | str | Unset = UNSET + due: Any | Unset = UNSET + relative_weeks_due: Any | Unset = UNSET + format_: Any | Unset = UNSET + course_graders: list[Any] | None | Unset = UNSET + actions: None | PatchedXblockActionsType0 | Unset = UNSET + explanatory_message: None | str | Unset = UNSET + group_access: None | PatchedXblockGroupAccessType0 | Unset = UNSET + user_partitions: list[Any] | None | Unset = UNSET + show_correctness: None | str | Unset = UNSET + discussion_enabled: bool | None | Unset = UNSET + ancestor_has_staff_lock: bool | None | Unset = UNSET + user_partition_info: None | PatchedXblockUserPartitionInfoType0 | Unset = UNSET + summary_configuration_enabled: Any | Unset = UNSET + is_prereq: bool | None | Unset = UNSET + prereq_usage_key: None | str | Unset = UNSET + prereq_min_score: int | None | Unset = UNSET + prereq_min_completion: int | None | Unset = UNSET + publish: None | PublishEnum | Unset = UNSET + duplicate_source_locator: None | str | Unset = UNSET + move_source_locator: None | str | Unset = UNSET + target_index: int | None | Unset = UNSET + boilerplate: Any | Unset = UNSET + staged_content: None | str | Unset = UNSET + hide_from_toc: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.patched_xblock_actions_type_0 import PatchedXblockActionsType0 + from ..models.patched_xblock_fields_type_0 import PatchedXblockFieldsType0 + from ..models.patched_xblock_group_access_type_0 import PatchedXblockGroupAccessType0 + from ..models.patched_xblock_metadata_type_0 import PatchedXblockMetadataType0 + from ..models.patched_xblock_user_partition_info_type_0 import PatchedXblockUserPartitionInfoType0 + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + parent_locator: None | str | Unset + if isinstance(self.parent_locator, Unset): + parent_locator = UNSET + else: + parent_locator = self.parent_locator + + display_name: None | str | Unset + if isinstance(self.display_name, Unset): + display_name = UNSET + else: + display_name = self.display_name + + category: None | str | Unset + if isinstance(self.category, Unset): + category = UNSET + else: + category = self.category + + data: None | str | Unset + if isinstance(self.data, Unset): + data = UNSET + else: + data = self.data + + metadata: dict[str, Any] | None | Unset + if isinstance(self.metadata, Unset): + metadata = UNSET + elif isinstance(self.metadata, PatchedXblockMetadataType0): + metadata = self.metadata.to_dict() + else: + metadata = self.metadata + + has_changes: bool | None | Unset + if isinstance(self.has_changes, Unset): + has_changes = UNSET + else: + has_changes = self.has_changes + + children: list[Any] | None | Unset + if isinstance(self.children, Unset): + children = UNSET + elif isinstance(self.children, list): + children = self.children + + else: + children = self.children + + fields: dict[str, Any] | None | Unset + if isinstance(self.fields, Unset): + fields = UNSET + elif isinstance(self.fields, PatchedXblockFieldsType0): + fields = self.fields.to_dict() + else: + fields = self.fields + + has_children: bool | None | Unset + if isinstance(self.has_children, Unset): + has_children = UNSET + else: + has_children = self.has_children + + video_sharing_enabled: bool | None | Unset + if isinstance(self.video_sharing_enabled, Unset): + video_sharing_enabled = UNSET + else: + video_sharing_enabled = self.video_sharing_enabled + + video_sharing_options: None | str | Unset + if isinstance(self.video_sharing_options, Unset): + video_sharing_options = UNSET + else: + video_sharing_options = self.video_sharing_options + + video_sharing_doc_url: None | str | Unset + if isinstance(self.video_sharing_doc_url, Unset): + video_sharing_doc_url = UNSET + else: + video_sharing_doc_url = self.video_sharing_doc_url + + edited_on: None | str | Unset + if isinstance(self.edited_on, Unset): + edited_on = UNSET + else: + edited_on = self.edited_on + + published: bool | None | Unset + if isinstance(self.published, Unset): + published = UNSET + else: + published = self.published + + published_on = self.published_on + + studio_url: None | str | Unset + if isinstance(self.studio_url, Unset): + studio_url = UNSET + else: + studio_url = self.studio_url + + released_to_students: bool | None | Unset + if isinstance(self.released_to_students, Unset): + released_to_students = UNSET + else: + released_to_students = self.released_to_students + + release_date = self.release_date + + nullout = self.nullout + + grader_type = self.grader_type + + visibility_state: None | str | Unset + if isinstance(self.visibility_state, Unset): + visibility_state = UNSET + else: + visibility_state = self.visibility_state + + has_explicit_staff_lock: bool | None | Unset + if isinstance(self.has_explicit_staff_lock, Unset): + has_explicit_staff_lock = UNSET + else: + has_explicit_staff_lock = self.has_explicit_staff_lock + + start: None | str | Unset + if isinstance(self.start, Unset): + start = UNSET + else: + start = self.start + + graded: bool | None | Unset + if isinstance(self.graded, Unset): + graded = UNSET + else: + graded = self.graded + + due_date: None | str | Unset + if isinstance(self.due_date, Unset): + due_date = UNSET + else: + due_date = self.due_date + + due = self.due + + relative_weeks_due = self.relative_weeks_due + + format_ = self.format_ + + course_graders: list[Any] | None | Unset + if isinstance(self.course_graders, Unset): + course_graders = UNSET + elif isinstance(self.course_graders, list): + course_graders = self.course_graders + + else: + course_graders = self.course_graders + + actions: dict[str, Any] | None | Unset + if isinstance(self.actions, Unset): + actions = UNSET + elif isinstance(self.actions, PatchedXblockActionsType0): + actions = self.actions.to_dict() + else: + actions = self.actions + + explanatory_message: None | str | Unset + if isinstance(self.explanatory_message, Unset): + explanatory_message = UNSET + else: + explanatory_message = self.explanatory_message + + group_access: dict[str, Any] | None | Unset + if isinstance(self.group_access, Unset): + group_access = UNSET + elif isinstance(self.group_access, PatchedXblockGroupAccessType0): + group_access = self.group_access.to_dict() + else: + group_access = self.group_access + + user_partitions: list[Any] | None | Unset + if isinstance(self.user_partitions, Unset): + user_partitions = UNSET + elif isinstance(self.user_partitions, list): + user_partitions = self.user_partitions + + else: + user_partitions = self.user_partitions + + show_correctness: None | str | Unset + if isinstance(self.show_correctness, Unset): + show_correctness = UNSET + else: + show_correctness = self.show_correctness + + discussion_enabled: bool | None | Unset + if isinstance(self.discussion_enabled, Unset): + discussion_enabled = UNSET + else: + discussion_enabled = self.discussion_enabled + + ancestor_has_staff_lock: bool | None | Unset + if isinstance(self.ancestor_has_staff_lock, Unset): + ancestor_has_staff_lock = UNSET + else: + ancestor_has_staff_lock = self.ancestor_has_staff_lock + + user_partition_info: dict[str, Any] | None | Unset + if isinstance(self.user_partition_info, Unset): + user_partition_info = UNSET + elif isinstance(self.user_partition_info, PatchedXblockUserPartitionInfoType0): + user_partition_info = self.user_partition_info.to_dict() + else: + user_partition_info = self.user_partition_info + + summary_configuration_enabled = self.summary_configuration_enabled + + is_prereq: bool | None | Unset + if isinstance(self.is_prereq, Unset): + is_prereq = UNSET + else: + is_prereq = self.is_prereq + + prereq_usage_key: None | str | Unset + if isinstance(self.prereq_usage_key, Unset): + prereq_usage_key = UNSET + else: + prereq_usage_key = self.prereq_usage_key + + prereq_min_score: int | None | Unset + if isinstance(self.prereq_min_score, Unset): + prereq_min_score = UNSET + else: + prereq_min_score = self.prereq_min_score + + prereq_min_completion: int | None | Unset + if isinstance(self.prereq_min_completion, Unset): + prereq_min_completion = UNSET + else: + prereq_min_completion = self.prereq_min_completion + + publish: None | str | Unset + if isinstance(self.publish, Unset): + publish = UNSET + elif isinstance(self.publish, PublishEnum): + publish = self.publish.value + else: + publish = self.publish + + duplicate_source_locator: None | str | Unset + if isinstance(self.duplicate_source_locator, Unset): + duplicate_source_locator = UNSET + else: + duplicate_source_locator = self.duplicate_source_locator + + move_source_locator: None | str | Unset + if isinstance(self.move_source_locator, Unset): + move_source_locator = UNSET + else: + move_source_locator = self.move_source_locator + + target_index: int | None | Unset + if isinstance(self.target_index, Unset): + target_index = UNSET + else: + target_index = self.target_index + + boilerplate = self.boilerplate + + staged_content: None | str | Unset + if isinstance(self.staged_content, Unset): + staged_content = UNSET + else: + staged_content = self.staged_content + + hide_from_toc: bool | None | Unset + if isinstance(self.hide_from_toc, Unset): + hide_from_toc = UNSET + else: + hide_from_toc = self.hide_from_toc + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if id is not UNSET: + field_dict["id"] = id + if parent_locator is not UNSET: + field_dict["parent_locator"] = parent_locator + if display_name is not UNSET: + field_dict["display_name"] = display_name + if category is not UNSET: + field_dict["category"] = category + if data is not UNSET: + field_dict["data"] = data + if metadata is not UNSET: + field_dict["metadata"] = metadata + if has_changes is not UNSET: + field_dict["has_changes"] = has_changes + if children is not UNSET: + field_dict["children"] = children + if fields is not UNSET: + field_dict["fields"] = fields + if has_children is not UNSET: + field_dict["has_children"] = has_children + if video_sharing_enabled is not UNSET: + field_dict["video_sharing_enabled"] = video_sharing_enabled + if video_sharing_options is not UNSET: + field_dict["video_sharing_options"] = video_sharing_options + if video_sharing_doc_url is not UNSET: + field_dict["video_sharing_doc_url"] = video_sharing_doc_url + if edited_on is not UNSET: + field_dict["edited_on"] = edited_on + if published is not UNSET: + field_dict["published"] = published + if published_on is not UNSET: + field_dict["published_on"] = published_on + if studio_url is not UNSET: + field_dict["studio_url"] = studio_url + if released_to_students is not UNSET: + field_dict["released_to_students"] = released_to_students + if release_date is not UNSET: + field_dict["release_date"] = release_date + if nullout is not UNSET: + field_dict["nullout"] = nullout + if grader_type is not UNSET: + field_dict["graderType"] = grader_type + if visibility_state is not UNSET: + field_dict["visibility_state"] = visibility_state + if has_explicit_staff_lock is not UNSET: + field_dict["has_explicit_staff_lock"] = has_explicit_staff_lock + if start is not UNSET: + field_dict["start"] = start + if graded is not UNSET: + field_dict["graded"] = graded + if due_date is not UNSET: + field_dict["due_date"] = due_date + if due is not UNSET: + field_dict["due"] = due + if relative_weeks_due is not UNSET: + field_dict["relative_weeks_due"] = relative_weeks_due + if format_ is not UNSET: + field_dict["format"] = format_ + if course_graders is not UNSET: + field_dict["course_graders"] = course_graders + if actions is not UNSET: + field_dict["actions"] = actions + if explanatory_message is not UNSET: + field_dict["explanatory_message"] = explanatory_message + if group_access is not UNSET: + field_dict["group_access"] = group_access + if user_partitions is not UNSET: + field_dict["user_partitions"] = user_partitions + if show_correctness is not UNSET: + field_dict["show_correctness"] = show_correctness + if discussion_enabled is not UNSET: + field_dict["discussion_enabled"] = discussion_enabled + if ancestor_has_staff_lock is not UNSET: + field_dict["ancestor_has_staff_lock"] = ancestor_has_staff_lock + if user_partition_info is not UNSET: + field_dict["user_partition_info"] = user_partition_info + if summary_configuration_enabled is not UNSET: + field_dict["summary_configuration_enabled"] = summary_configuration_enabled + if is_prereq is not UNSET: + field_dict["isPrereq"] = is_prereq + if prereq_usage_key is not UNSET: + field_dict["prereqUsageKey"] = prereq_usage_key + if prereq_min_score is not UNSET: + field_dict["prereqMinScore"] = prereq_min_score + if prereq_min_completion is not UNSET: + field_dict["prereqMinCompletion"] = prereq_min_completion + if publish is not UNSET: + field_dict["publish"] = publish + if duplicate_source_locator is not UNSET: + field_dict["duplicate_source_locator"] = duplicate_source_locator + if move_source_locator is not UNSET: + field_dict["move_source_locator"] = move_source_locator + if target_index is not UNSET: + field_dict["target_index"] = target_index + if boilerplate is not UNSET: + field_dict["boilerplate"] = boilerplate + if staged_content is not UNSET: + field_dict["staged_content"] = staged_content + if hide_from_toc is not UNSET: + field_dict["hide_from_toc"] = hide_from_toc + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + from ..models.patched_xblock_actions_type_0 import PatchedXblockActionsType0 + from ..models.patched_xblock_fields_type_0 import PatchedXblockFieldsType0 + from ..models.patched_xblock_group_access_type_0 import PatchedXblockGroupAccessType0 + from ..models.patched_xblock_metadata_type_0 import PatchedXblockMetadataType0 + from ..models.patched_xblock_user_partition_info_type_0 import PatchedXblockUserPartitionInfoType0 + + files: types.RequestFiles = [] + + if not isinstance(self.id, Unset): + if isinstance(self.id, str): + files.append(("id", (None, str(self.id).encode(), "text/plain"))) + else: + files.append(("id", (None, str(self.id).encode(), "text/plain"))) + + if not isinstance(self.parent_locator, Unset): + if isinstance(self.parent_locator, str): + files.append(("parent_locator", (None, str(self.parent_locator).encode(), "text/plain"))) + else: + files.append(("parent_locator", (None, str(self.parent_locator).encode(), "text/plain"))) + + if not isinstance(self.display_name, Unset): + if isinstance(self.display_name, str): + files.append(("display_name", (None, str(self.display_name).encode(), "text/plain"))) + else: + files.append(("display_name", (None, str(self.display_name).encode(), "text/plain"))) + + if not isinstance(self.category, Unset): + if isinstance(self.category, str): + files.append(("category", (None, str(self.category).encode(), "text/plain"))) + else: + files.append(("category", (None, str(self.category).encode(), "text/plain"))) + + if not isinstance(self.data, Unset): + if isinstance(self.data, str): + files.append(("data", (None, str(self.data).encode(), "text/plain"))) + else: + files.append(("data", (None, str(self.data).encode(), "text/plain"))) + + if not isinstance(self.metadata, Unset): + if isinstance(self.metadata, PatchedXblockMetadataType0): + files.append(("metadata", (None, json.dumps(self.metadata.to_dict()).encode(), "application/json"))) + else: + files.append(("metadata", (None, str(self.metadata).encode(), "text/plain"))) + + if not isinstance(self.has_changes, Unset): + if isinstance(self.has_changes, bool): + files.append(("has_changes", (None, str(self.has_changes).encode(), "text/plain"))) + else: + files.append(("has_changes", (None, str(self.has_changes).encode(), "text/plain"))) + + if not isinstance(self.children, Unset): + if isinstance(self.children, list): + for children_type_0_item_element in self.children: + files.append(("children", (None, str(children_type_0_item_element).encode(), "text/plain"))) + else: + files.append(("children", (None, str(self.children).encode(), "text/plain"))) + + if not isinstance(self.fields, Unset): + if isinstance(self.fields, PatchedXblockFieldsType0): + files.append(("fields", (None, json.dumps(self.fields.to_dict()).encode(), "application/json"))) + else: + files.append(("fields", (None, str(self.fields).encode(), "text/plain"))) + + if not isinstance(self.has_children, Unset): + if isinstance(self.has_children, bool): + files.append(("has_children", (None, str(self.has_children).encode(), "text/plain"))) + else: + files.append(("has_children", (None, str(self.has_children).encode(), "text/plain"))) + + if not isinstance(self.video_sharing_enabled, Unset): + if isinstance(self.video_sharing_enabled, bool): + files.append(("video_sharing_enabled", (None, str(self.video_sharing_enabled).encode(), "text/plain"))) + else: + files.append(("video_sharing_enabled", (None, str(self.video_sharing_enabled).encode(), "text/plain"))) + + if not isinstance(self.video_sharing_options, Unset): + if isinstance(self.video_sharing_options, str): + files.append(("video_sharing_options", (None, str(self.video_sharing_options).encode(), "text/plain"))) + else: + files.append(("video_sharing_options", (None, str(self.video_sharing_options).encode(), "text/plain"))) + + if not isinstance(self.video_sharing_doc_url, Unset): + if isinstance(self.video_sharing_doc_url, str): + files.append(("video_sharing_doc_url", (None, str(self.video_sharing_doc_url).encode(), "text/plain"))) + else: + files.append(("video_sharing_doc_url", (None, str(self.video_sharing_doc_url).encode(), "text/plain"))) + + if not isinstance(self.edited_on, Unset): + if isinstance(self.edited_on, str): + files.append(("edited_on", (None, str(self.edited_on).encode(), "text/plain"))) + else: + files.append(("edited_on", (None, str(self.edited_on).encode(), "text/plain"))) + + if not isinstance(self.published, Unset): + if isinstance(self.published, bool): + files.append(("published", (None, str(self.published).encode(), "text/plain"))) + else: + files.append(("published", (None, str(self.published).encode(), "text/plain"))) + + if not isinstance(self.published_on, Unset): + files.append(("published_on", (None, str(self.published_on).encode(), "text/plain"))) + + if not isinstance(self.studio_url, Unset): + if isinstance(self.studio_url, str): + files.append(("studio_url", (None, str(self.studio_url).encode(), "text/plain"))) + else: + files.append(("studio_url", (None, str(self.studio_url).encode(), "text/plain"))) + + if not isinstance(self.released_to_students, Unset): + if isinstance(self.released_to_students, bool): + files.append(("released_to_students", (None, str(self.released_to_students).encode(), "text/plain"))) + else: + files.append(("released_to_students", (None, str(self.released_to_students).encode(), "text/plain"))) + + if not isinstance(self.release_date, Unset): + files.append(("release_date", (None, str(self.release_date).encode(), "text/plain"))) + + if not isinstance(self.nullout, Unset): + files.append(("nullout", (None, str(self.nullout).encode(), "text/plain"))) + + if not isinstance(self.grader_type, Unset): + files.append(("graderType", (None, str(self.grader_type).encode(), "text/plain"))) + + if not isinstance(self.visibility_state, Unset): + if isinstance(self.visibility_state, str): + files.append(("visibility_state", (None, str(self.visibility_state).encode(), "text/plain"))) + else: + files.append(("visibility_state", (None, str(self.visibility_state).encode(), "text/plain"))) + + if not isinstance(self.has_explicit_staff_lock, Unset): + if isinstance(self.has_explicit_staff_lock, bool): + files.append( + ("has_explicit_staff_lock", (None, str(self.has_explicit_staff_lock).encode(), "text/plain")) + ) + else: + files.append( + ("has_explicit_staff_lock", (None, str(self.has_explicit_staff_lock).encode(), "text/plain")) + ) + + if not isinstance(self.start, Unset): + if isinstance(self.start, str): + files.append(("start", (None, str(self.start).encode(), "text/plain"))) + else: + files.append(("start", (None, str(self.start).encode(), "text/plain"))) + + if not isinstance(self.graded, Unset): + if isinstance(self.graded, bool): + files.append(("graded", (None, str(self.graded).encode(), "text/plain"))) + else: + files.append(("graded", (None, str(self.graded).encode(), "text/plain"))) + + if not isinstance(self.due_date, Unset): + if isinstance(self.due_date, str): + files.append(("due_date", (None, str(self.due_date).encode(), "text/plain"))) + else: + files.append(("due_date", (None, str(self.due_date).encode(), "text/plain"))) + + if not isinstance(self.due, Unset): + files.append(("due", (None, str(self.due).encode(), "text/plain"))) + + if not isinstance(self.relative_weeks_due, Unset): + files.append(("relative_weeks_due", (None, str(self.relative_weeks_due).encode(), "text/plain"))) + + if not isinstance(self.format_, Unset): + files.append(("format", (None, str(self.format_).encode(), "text/plain"))) + + if not isinstance(self.course_graders, Unset): + if isinstance(self.course_graders, list): + for course_graders_type_0_item_element in self.course_graders: + files.append( + ("course_graders", (None, str(course_graders_type_0_item_element).encode(), "text/plain")) + ) + else: + files.append(("course_graders", (None, str(self.course_graders).encode(), "text/plain"))) + + if not isinstance(self.actions, Unset): + if isinstance(self.actions, PatchedXblockActionsType0): + files.append(("actions", (None, json.dumps(self.actions.to_dict()).encode(), "application/json"))) + else: + files.append(("actions", (None, str(self.actions).encode(), "text/plain"))) + + if not isinstance(self.explanatory_message, Unset): + if isinstance(self.explanatory_message, str): + files.append(("explanatory_message", (None, str(self.explanatory_message).encode(), "text/plain"))) + else: + files.append(("explanatory_message", (None, str(self.explanatory_message).encode(), "text/plain"))) + + if not isinstance(self.group_access, Unset): + if isinstance(self.group_access, PatchedXblockGroupAccessType0): + files.append( + ("group_access", (None, json.dumps(self.group_access.to_dict()).encode(), "application/json")) + ) + else: + files.append(("group_access", (None, str(self.group_access).encode(), "text/plain"))) + + if not isinstance(self.user_partitions, Unset): + if isinstance(self.user_partitions, list): + for user_partitions_type_0_item_element in self.user_partitions: + files.append( + ("user_partitions", (None, str(user_partitions_type_0_item_element).encode(), "text/plain")) + ) + else: + files.append(("user_partitions", (None, str(self.user_partitions).encode(), "text/plain"))) + + if not isinstance(self.show_correctness, Unset): + if isinstance(self.show_correctness, str): + files.append(("show_correctness", (None, str(self.show_correctness).encode(), "text/plain"))) + else: + files.append(("show_correctness", (None, str(self.show_correctness).encode(), "text/plain"))) + + if not isinstance(self.discussion_enabled, Unset): + if isinstance(self.discussion_enabled, bool): + files.append(("discussion_enabled", (None, str(self.discussion_enabled).encode(), "text/plain"))) + else: + files.append(("discussion_enabled", (None, str(self.discussion_enabled).encode(), "text/plain"))) + + if not isinstance(self.ancestor_has_staff_lock, Unset): + if isinstance(self.ancestor_has_staff_lock, bool): + files.append( + ("ancestor_has_staff_lock", (None, str(self.ancestor_has_staff_lock).encode(), "text/plain")) + ) + else: + files.append( + ("ancestor_has_staff_lock", (None, str(self.ancestor_has_staff_lock).encode(), "text/plain")) + ) + + if not isinstance(self.user_partition_info, Unset): + if isinstance(self.user_partition_info, PatchedXblockUserPartitionInfoType0): + files.append( + ( + "user_partition_info", + (None, json.dumps(self.user_partition_info.to_dict()).encode(), "application/json"), + ) + ) + else: + files.append(("user_partition_info", (None, str(self.user_partition_info).encode(), "text/plain"))) + + if not isinstance(self.summary_configuration_enabled, Unset): + files.append( + ( + "summary_configuration_enabled", + (None, str(self.summary_configuration_enabled).encode(), "text/plain"), + ) + ) + + if not isinstance(self.is_prereq, Unset): + if isinstance(self.is_prereq, bool): + files.append(("isPrereq", (None, str(self.is_prereq).encode(), "text/plain"))) + else: + files.append(("isPrereq", (None, str(self.is_prereq).encode(), "text/plain"))) + + if not isinstance(self.prereq_usage_key, Unset): + if isinstance(self.prereq_usage_key, str): + files.append(("prereqUsageKey", (None, str(self.prereq_usage_key).encode(), "text/plain"))) + else: + files.append(("prereqUsageKey", (None, str(self.prereq_usage_key).encode(), "text/plain"))) + + if not isinstance(self.prereq_min_score, Unset): + if isinstance(self.prereq_min_score, int): + files.append(("prereqMinScore", (None, str(self.prereq_min_score).encode(), "text/plain"))) + else: + files.append(("prereqMinScore", (None, str(self.prereq_min_score).encode(), "text/plain"))) + + if not isinstance(self.prereq_min_completion, Unset): + if isinstance(self.prereq_min_completion, int): + files.append(("prereqMinCompletion", (None, str(self.prereq_min_completion).encode(), "text/plain"))) + else: + files.append(("prereqMinCompletion", (None, str(self.prereq_min_completion).encode(), "text/plain"))) + + if not isinstance(self.publish, Unset): + if isinstance(self.publish, PublishEnum): + files.append(("publish", (None, str(self.publish.value).encode(), "text/plain"))) + else: + files.append(("publish", (None, str(self.publish).encode(), "text/plain"))) + + if not isinstance(self.duplicate_source_locator, Unset): + if isinstance(self.duplicate_source_locator, str): + files.append( + ("duplicate_source_locator", (None, str(self.duplicate_source_locator).encode(), "text/plain")) + ) + else: + files.append( + ("duplicate_source_locator", (None, str(self.duplicate_source_locator).encode(), "text/plain")) + ) + + if not isinstance(self.move_source_locator, Unset): + if isinstance(self.move_source_locator, str): + files.append(("move_source_locator", (None, str(self.move_source_locator).encode(), "text/plain"))) + else: + files.append(("move_source_locator", (None, str(self.move_source_locator).encode(), "text/plain"))) + + if not isinstance(self.target_index, Unset): + if isinstance(self.target_index, int): + files.append(("target_index", (None, str(self.target_index).encode(), "text/plain"))) + else: + files.append(("target_index", (None, str(self.target_index).encode(), "text/plain"))) + + if not isinstance(self.boilerplate, Unset): + files.append(("boilerplate", (None, str(self.boilerplate).encode(), "text/plain"))) + + if not isinstance(self.staged_content, Unset): + if isinstance(self.staged_content, str): + files.append(("staged_content", (None, str(self.staged_content).encode(), "text/plain"))) + else: + files.append(("staged_content", (None, str(self.staged_content).encode(), "text/plain"))) + + if not isinstance(self.hide_from_toc, Unset): + if isinstance(self.hide_from_toc, bool): + files.append(("hide_from_toc", (None, str(self.hide_from_toc).encode(), "text/plain"))) + else: + files.append(("hide_from_toc", (None, str(self.hide_from_toc).encode(), "text/plain"))) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.patched_xblock_actions_type_0 import PatchedXblockActionsType0 + from ..models.patched_xblock_fields_type_0 import PatchedXblockFieldsType0 + from ..models.patched_xblock_group_access_type_0 import PatchedXblockGroupAccessType0 + from ..models.patched_xblock_metadata_type_0 import PatchedXblockMetadataType0 + from ..models.patched_xblock_user_partition_info_type_0 import PatchedXblockUserPartitionInfoType0 + + d = dict(src_dict) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + def _parse_parent_locator(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + parent_locator = _parse_parent_locator(d.pop("parent_locator", UNSET)) + + def _parse_display_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + display_name = _parse_display_name(d.pop("display_name", UNSET)) + + def _parse_category(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + category = _parse_category(d.pop("category", UNSET)) + + def _parse_data(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + data = _parse_data(d.pop("data", UNSET)) + + def _parse_metadata(data: object) -> None | PatchedXblockMetadataType0 | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + metadata_type_0 = PatchedXblockMetadataType0.from_dict(data) + + return metadata_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PatchedXblockMetadataType0 | Unset, data) + + metadata = _parse_metadata(d.pop("metadata", UNSET)) + + def _parse_has_changes(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + has_changes = _parse_has_changes(d.pop("has_changes", UNSET)) + + def _parse_children(data: object) -> list[Any] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + children_type_0 = cast(list[Any], data) + + return children_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[Any] | None | Unset, data) + + children = _parse_children(d.pop("children", UNSET)) + + def _parse_fields(data: object) -> None | PatchedXblockFieldsType0 | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fields_type_0 = PatchedXblockFieldsType0.from_dict(data) + + return fields_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PatchedXblockFieldsType0 | Unset, data) + + fields = _parse_fields(d.pop("fields", UNSET)) + + def _parse_has_children(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + has_children = _parse_has_children(d.pop("has_children", UNSET)) + + def _parse_video_sharing_enabled(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + video_sharing_enabled = _parse_video_sharing_enabled(d.pop("video_sharing_enabled", UNSET)) + + def _parse_video_sharing_options(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + video_sharing_options = _parse_video_sharing_options(d.pop("video_sharing_options", UNSET)) + + def _parse_video_sharing_doc_url(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + video_sharing_doc_url = _parse_video_sharing_doc_url(d.pop("video_sharing_doc_url", UNSET)) + + def _parse_edited_on(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + edited_on = _parse_edited_on(d.pop("edited_on", UNSET)) + + def _parse_published(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + published = _parse_published(d.pop("published", UNSET)) + + published_on = d.pop("published_on", UNSET) + + def _parse_studio_url(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + studio_url = _parse_studio_url(d.pop("studio_url", UNSET)) + + def _parse_released_to_students(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + released_to_students = _parse_released_to_students(d.pop("released_to_students", UNSET)) + + release_date = d.pop("release_date", UNSET) + + nullout = d.pop("nullout", UNSET) + + grader_type = d.pop("graderType", UNSET) + + def _parse_visibility_state(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + visibility_state = _parse_visibility_state(d.pop("visibility_state", UNSET)) + + def _parse_has_explicit_staff_lock(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + has_explicit_staff_lock = _parse_has_explicit_staff_lock(d.pop("has_explicit_staff_lock", UNSET)) + + def _parse_start(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + start = _parse_start(d.pop("start", UNSET)) + + def _parse_graded(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + graded = _parse_graded(d.pop("graded", UNSET)) + + def _parse_due_date(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + due_date = _parse_due_date(d.pop("due_date", UNSET)) + + due = d.pop("due", UNSET) + + relative_weeks_due = d.pop("relative_weeks_due", UNSET) + + format_ = d.pop("format", UNSET) + + def _parse_course_graders(data: object) -> list[Any] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + course_graders_type_0 = cast(list[Any], data) + + return course_graders_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[Any] | None | Unset, data) + + course_graders = _parse_course_graders(d.pop("course_graders", UNSET)) + + def _parse_actions(data: object) -> None | PatchedXblockActionsType0 | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + actions_type_0 = PatchedXblockActionsType0.from_dict(data) + + return actions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PatchedXblockActionsType0 | Unset, data) + + actions = _parse_actions(d.pop("actions", UNSET)) + + def _parse_explanatory_message(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + explanatory_message = _parse_explanatory_message(d.pop("explanatory_message", UNSET)) + + def _parse_group_access(data: object) -> None | PatchedXblockGroupAccessType0 | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + group_access_type_0 = PatchedXblockGroupAccessType0.from_dict(data) + + return group_access_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PatchedXblockGroupAccessType0 | Unset, data) + + group_access = _parse_group_access(d.pop("group_access", UNSET)) + + def _parse_user_partitions(data: object) -> list[Any] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + user_partitions_type_0 = cast(list[Any], data) + + return user_partitions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[Any] | None | Unset, data) + + user_partitions = _parse_user_partitions(d.pop("user_partitions", UNSET)) + + def _parse_show_correctness(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + show_correctness = _parse_show_correctness(d.pop("show_correctness", UNSET)) + + def _parse_discussion_enabled(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + discussion_enabled = _parse_discussion_enabled(d.pop("discussion_enabled", UNSET)) + + def _parse_ancestor_has_staff_lock(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + ancestor_has_staff_lock = _parse_ancestor_has_staff_lock(d.pop("ancestor_has_staff_lock", UNSET)) + + def _parse_user_partition_info(data: object) -> None | PatchedXblockUserPartitionInfoType0 | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + user_partition_info_type_0 = PatchedXblockUserPartitionInfoType0.from_dict(data) + + return user_partition_info_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PatchedXblockUserPartitionInfoType0 | Unset, data) + + user_partition_info = _parse_user_partition_info(d.pop("user_partition_info", UNSET)) + + summary_configuration_enabled = d.pop("summary_configuration_enabled", UNSET) + + def _parse_is_prereq(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + is_prereq = _parse_is_prereq(d.pop("isPrereq", UNSET)) + + def _parse_prereq_usage_key(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + prereq_usage_key = _parse_prereq_usage_key(d.pop("prereqUsageKey", UNSET)) + + def _parse_prereq_min_score(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + prereq_min_score = _parse_prereq_min_score(d.pop("prereqMinScore", UNSET)) + + def _parse_prereq_min_completion(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + prereq_min_completion = _parse_prereq_min_completion(d.pop("prereqMinCompletion", UNSET)) + + def _parse_publish(data: object) -> None | PublishEnum | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + publish_type_0 = PublishEnum(data) + + return publish_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEnum | Unset, data) + + publish = _parse_publish(d.pop("publish", UNSET)) + + def _parse_duplicate_source_locator(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + duplicate_source_locator = _parse_duplicate_source_locator(d.pop("duplicate_source_locator", UNSET)) + + def _parse_move_source_locator(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + move_source_locator = _parse_move_source_locator(d.pop("move_source_locator", UNSET)) + + def _parse_target_index(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + target_index = _parse_target_index(d.pop("target_index", UNSET)) + + boilerplate = d.pop("boilerplate", UNSET) + + def _parse_staged_content(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + staged_content = _parse_staged_content(d.pop("staged_content", UNSET)) + + def _parse_hide_from_toc(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + hide_from_toc = _parse_hide_from_toc(d.pop("hide_from_toc", UNSET)) + + patched_xblock = cls( + id=id, + parent_locator=parent_locator, + display_name=display_name, + category=category, + data=data, + metadata=metadata, + has_changes=has_changes, + children=children, + fields=fields, + has_children=has_children, + video_sharing_enabled=video_sharing_enabled, + video_sharing_options=video_sharing_options, + video_sharing_doc_url=video_sharing_doc_url, + edited_on=edited_on, + published=published, + published_on=published_on, + studio_url=studio_url, + released_to_students=released_to_students, + release_date=release_date, + nullout=nullout, + grader_type=grader_type, + visibility_state=visibility_state, + has_explicit_staff_lock=has_explicit_staff_lock, + start=start, + graded=graded, + due_date=due_date, + due=due, + relative_weeks_due=relative_weeks_due, + format_=format_, + course_graders=course_graders, + actions=actions, + explanatory_message=explanatory_message, + group_access=group_access, + user_partitions=user_partitions, + show_correctness=show_correctness, + discussion_enabled=discussion_enabled, + ancestor_has_staff_lock=ancestor_has_staff_lock, + user_partition_info=user_partition_info, + summary_configuration_enabled=summary_configuration_enabled, + is_prereq=is_prereq, + prereq_usage_key=prereq_usage_key, + prereq_min_score=prereq_min_score, + prereq_min_completion=prereq_min_completion, + publish=publish, + duplicate_source_locator=duplicate_source_locator, + move_source_locator=move_source_locator, + target_index=target_index, + boilerplate=boilerplate, + staged_content=staged_content, + hide_from_toc=hide_from_toc, + ) + + patched_xblock.additional_properties = d + return patched_xblock + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patched_xblock_actions_type_0.py b/openedx_platform_sdk/models/patched_xblock_actions_type_0.py new file mode 100644 index 0000000..e33670b --- /dev/null +++ b/openedx_platform_sdk/models/patched_xblock_actions_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PatchedXblockActionsType0") + + +@_attrs_define +class PatchedXblockActionsType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + patched_xblock_actions_type_0 = cls() + + patched_xblock_actions_type_0.additional_properties = d + return patched_xblock_actions_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patched_xblock_fields_type_0.py b/openedx_platform_sdk/models/patched_xblock_fields_type_0.py new file mode 100644 index 0000000..dca9300 --- /dev/null +++ b/openedx_platform_sdk/models/patched_xblock_fields_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PatchedXblockFieldsType0") + + +@_attrs_define +class PatchedXblockFieldsType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + patched_xblock_fields_type_0 = cls() + + patched_xblock_fields_type_0.additional_properties = d + return patched_xblock_fields_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patched_xblock_group_access_type_0.py b/openedx_platform_sdk/models/patched_xblock_group_access_type_0.py new file mode 100644 index 0000000..19c75db --- /dev/null +++ b/openedx_platform_sdk/models/patched_xblock_group_access_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PatchedXblockGroupAccessType0") + + +@_attrs_define +class PatchedXblockGroupAccessType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + patched_xblock_group_access_type_0 = cls() + + patched_xblock_group_access_type_0.additional_properties = d + return patched_xblock_group_access_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patched_xblock_metadata_type_0.py b/openedx_platform_sdk/models/patched_xblock_metadata_type_0.py new file mode 100644 index 0000000..80f693c --- /dev/null +++ b/openedx_platform_sdk/models/patched_xblock_metadata_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PatchedXblockMetadataType0") + + +@_attrs_define +class PatchedXblockMetadataType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + patched_xblock_metadata_type_0 = cls() + + patched_xblock_metadata_type_0.additional_properties = d + return patched_xblock_metadata_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patched_xblock_user_partition_info_type_0.py b/openedx_platform_sdk/models/patched_xblock_user_partition_info_type_0.py new file mode 100644 index 0000000..39611a2 --- /dev/null +++ b/openedx_platform_sdk/models/patched_xblock_user_partition_info_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PatchedXblockUserPartitionInfoType0") + + +@_attrs_define +class PatchedXblockUserPartitionInfoType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + patched_xblock_user_partition_info_type_0 = cls() + + patched_xblock_user_partition_info_type_0.additional_properties = d + return patched_xblock_user_partition_info_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py b/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py new file mode 100644 index 0000000..e645ade --- /dev/null +++ b/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import json +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 + + +T = TypeVar("T", bound="PatchedauthoringGradingCourseGradingV0") + + +@_attrs_define +class PatchedauthoringGradingCourseGradingV0: + """Serializer for course grading model data + + Attributes: + graders (list[AuthoringGradingGradersV0] | None | Unset): + """ + + graders: list[AuthoringGradingGradersV0] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + graders: list[dict[str, Any]] | None | Unset + if isinstance(self.graders, Unset): + graders = UNSET + elif isinstance(self.graders, list): + graders = [] + for graders_type_0_item_data in self.graders: + graders_type_0_item = graders_type_0_item_data.to_dict() + graders.append(graders_type_0_item) + + else: + graders = self.graders + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if graders is not UNSET: + field_dict["graders"] = graders + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + if not isinstance(self.graders, Unset): + if isinstance(self.graders, list): + for graders_type_0_item_element in self.graders: + files.append( + ( + "graders", + (None, json.dumps(graders_type_0_item_element.to_dict()).encode(), "application/json"), + ) + ) + else: + files.append(("graders", (None, str(self.graders).encode(), "text/plain"))) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 + + d = dict(src_dict) + + def _parse_graders(data: object) -> list[AuthoringGradingGradersV0] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + graders_type_0 = [] + _graders_type_0 = data + for graders_type_0_item_data in _graders_type_0: + graders_type_0_item = AuthoringGradingGradersV0.from_dict(graders_type_0_item_data) + + graders_type_0.append(graders_type_0_item) + + return graders_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[AuthoringGradingGradersV0] | None | Unset, data) + + graders = _parse_graders(d.pop("graders", UNSET)) + + patchedauthoring_grading_course_grading_v0 = cls( + graders=graders, + ) + + patchedauthoring_grading_course_grading_v0.additional_properties = d + return patchedauthoring_grading_course_grading_v0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/publish_enum.py b/openedx_platform_sdk/models/publish_enum.py new file mode 100644 index 0000000..c699978 --- /dev/null +++ b/openedx_platform_sdk/models/publish_enum.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class PublishEnum(str, Enum): + DISCARD_CHANGES = "discard_changes" + MAKE_PUBLIC = "make_public" + REPUBLISH = "republish" + + def __str__(self) -> str: + return str(self.value) diff --git a/openedx_platform_sdk/models/studio_home.py b/openedx_platform_sdk/models/studio_home.py new file mode 100644 index 0000000..8af0fc8 --- /dev/null +++ b/openedx_platform_sdk/models/studio_home.py @@ -0,0 +1,380 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.course_common import CourseCommon + from ..models.library_view import LibraryView + from ..models.unsucceeded_course import UnsucceededCourse + + +T = TypeVar("T", bound="StudioHome") + + +@_attrs_define +class StudioHome: + """Serializer for Studio home + + Attributes: + allow_course_reruns (bool): + allow_to_create_new_org (bool): + allow_unicode_course_id (bool): + allowed_organizations (list[str]): + allowed_organizations_for_libraries (list[str]): + can_access_advanced_settings (bool): + can_create_organizations (bool): + course_creator_status (str): + libraries_enabled (bool): + libraries_v1_enabled (bool): + libraries_v2_enabled (bool): + taxonomies_enabled (bool): + taxonomy_list_mfe_url (str): + request_course_creator_url (str): + rerun_creator_status (bool): + show_new_library_button (bool): + show_new_library_v2_button (bool): + split_studio_home (bool): + studio_name (str): + studio_short_name (str): + studio_request_email (str): + tech_support_email (str): + platform_name (str): + user_is_active (bool): + archived_courses (list[CourseCommon] | Unset): + courses (list[CourseCommon] | Unset): + in_process_course_actions (list[UnsucceededCourse] | None | Unset): + libraries (list[LibraryView] | None | Unset): + """ + + allow_course_reruns: bool + allow_to_create_new_org: bool + allow_unicode_course_id: bool + allowed_organizations: list[str] + allowed_organizations_for_libraries: list[str] + can_access_advanced_settings: bool + can_create_organizations: bool + course_creator_status: str + libraries_enabled: bool + libraries_v1_enabled: bool + libraries_v2_enabled: bool + taxonomies_enabled: bool + taxonomy_list_mfe_url: str + request_course_creator_url: str + rerun_creator_status: bool + show_new_library_button: bool + show_new_library_v2_button: bool + split_studio_home: bool + studio_name: str + studio_short_name: str + studio_request_email: str + tech_support_email: str + platform_name: str + user_is_active: bool + archived_courses: list[CourseCommon] | Unset = UNSET + courses: list[CourseCommon] | Unset = UNSET + in_process_course_actions: list[UnsucceededCourse] | None | Unset = UNSET + libraries: list[LibraryView] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + allow_course_reruns = self.allow_course_reruns + + allow_to_create_new_org = self.allow_to_create_new_org + + allow_unicode_course_id = self.allow_unicode_course_id + + allowed_organizations = self.allowed_organizations + + allowed_organizations_for_libraries = self.allowed_organizations_for_libraries + + can_access_advanced_settings = self.can_access_advanced_settings + + can_create_organizations = self.can_create_organizations + + course_creator_status = self.course_creator_status + + libraries_enabled = self.libraries_enabled + + libraries_v1_enabled = self.libraries_v1_enabled + + libraries_v2_enabled = self.libraries_v2_enabled + + taxonomies_enabled = self.taxonomies_enabled + + taxonomy_list_mfe_url = self.taxonomy_list_mfe_url + + request_course_creator_url = self.request_course_creator_url + + rerun_creator_status = self.rerun_creator_status + + show_new_library_button = self.show_new_library_button + + show_new_library_v2_button = self.show_new_library_v2_button + + split_studio_home = self.split_studio_home + + studio_name = self.studio_name + + studio_short_name = self.studio_short_name + + studio_request_email = self.studio_request_email + + tech_support_email = self.tech_support_email + + platform_name = self.platform_name + + user_is_active = self.user_is_active + + archived_courses: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.archived_courses, Unset): + archived_courses = [] + for archived_courses_item_data in self.archived_courses: + archived_courses_item = archived_courses_item_data.to_dict() + archived_courses.append(archived_courses_item) + + courses: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.courses, Unset): + courses = [] + for courses_item_data in self.courses: + courses_item = courses_item_data.to_dict() + courses.append(courses_item) + + in_process_course_actions: list[dict[str, Any]] | None | Unset + if isinstance(self.in_process_course_actions, Unset): + in_process_course_actions = UNSET + elif isinstance(self.in_process_course_actions, list): + in_process_course_actions = [] + for in_process_course_actions_type_0_item_data in self.in_process_course_actions: + in_process_course_actions_type_0_item = in_process_course_actions_type_0_item_data.to_dict() + in_process_course_actions.append(in_process_course_actions_type_0_item) + + else: + in_process_course_actions = self.in_process_course_actions + + libraries: list[dict[str, Any]] | None | Unset + if isinstance(self.libraries, Unset): + libraries = UNSET + elif isinstance(self.libraries, list): + libraries = [] + for libraries_type_0_item_data in self.libraries: + libraries_type_0_item = libraries_type_0_item_data.to_dict() + libraries.append(libraries_type_0_item) + + else: + libraries = self.libraries + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "allow_course_reruns": allow_course_reruns, + "allow_to_create_new_org": allow_to_create_new_org, + "allow_unicode_course_id": allow_unicode_course_id, + "allowed_organizations": allowed_organizations, + "allowed_organizations_for_libraries": allowed_organizations_for_libraries, + "can_access_advanced_settings": can_access_advanced_settings, + "can_create_organizations": can_create_organizations, + "course_creator_status": course_creator_status, + "libraries_enabled": libraries_enabled, + "libraries_v1_enabled": libraries_v1_enabled, + "libraries_v2_enabled": libraries_v2_enabled, + "taxonomies_enabled": taxonomies_enabled, + "taxonomy_list_mfe_url": taxonomy_list_mfe_url, + "request_course_creator_url": request_course_creator_url, + "rerun_creator_status": rerun_creator_status, + "show_new_library_button": show_new_library_button, + "show_new_library_v2_button": show_new_library_v2_button, + "split_studio_home": split_studio_home, + "studio_name": studio_name, + "studio_short_name": studio_short_name, + "studio_request_email": studio_request_email, + "tech_support_email": tech_support_email, + "platform_name": platform_name, + "user_is_active": user_is_active, + } + ) + if archived_courses is not UNSET: + field_dict["archived_courses"] = archived_courses + if courses is not UNSET: + field_dict["courses"] = courses + if in_process_course_actions is not UNSET: + field_dict["in_process_course_actions"] = in_process_course_actions + if libraries is not UNSET: + field_dict["libraries"] = libraries + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.course_common import CourseCommon + from ..models.library_view import LibraryView + from ..models.unsucceeded_course import UnsucceededCourse + + d = dict(src_dict) + allow_course_reruns = d.pop("allow_course_reruns") + + allow_to_create_new_org = d.pop("allow_to_create_new_org") + + allow_unicode_course_id = d.pop("allow_unicode_course_id") + + allowed_organizations = cast(list[str], d.pop("allowed_organizations")) + + allowed_organizations_for_libraries = cast(list[str], d.pop("allowed_organizations_for_libraries")) + + can_access_advanced_settings = d.pop("can_access_advanced_settings") + + can_create_organizations = d.pop("can_create_organizations") + + course_creator_status = d.pop("course_creator_status") + + libraries_enabled = d.pop("libraries_enabled") + + libraries_v1_enabled = d.pop("libraries_v1_enabled") + + libraries_v2_enabled = d.pop("libraries_v2_enabled") + + taxonomies_enabled = d.pop("taxonomies_enabled") + + taxonomy_list_mfe_url = d.pop("taxonomy_list_mfe_url") + + request_course_creator_url = d.pop("request_course_creator_url") + + rerun_creator_status = d.pop("rerun_creator_status") + + show_new_library_button = d.pop("show_new_library_button") + + show_new_library_v2_button = d.pop("show_new_library_v2_button") + + split_studio_home = d.pop("split_studio_home") + + studio_name = d.pop("studio_name") + + studio_short_name = d.pop("studio_short_name") + + studio_request_email = d.pop("studio_request_email") + + tech_support_email = d.pop("tech_support_email") + + platform_name = d.pop("platform_name") + + user_is_active = d.pop("user_is_active") + + _archived_courses = d.pop("archived_courses", UNSET) + archived_courses: list[CourseCommon] | Unset = UNSET + if _archived_courses is not UNSET: + archived_courses = [] + for archived_courses_item_data in _archived_courses: + archived_courses_item = CourseCommon.from_dict(archived_courses_item_data) + + archived_courses.append(archived_courses_item) + + _courses = d.pop("courses", UNSET) + courses: list[CourseCommon] | Unset = UNSET + if _courses is not UNSET: + courses = [] + for courses_item_data in _courses: + courses_item = CourseCommon.from_dict(courses_item_data) + + courses.append(courses_item) + + def _parse_in_process_course_actions(data: object) -> list[UnsucceededCourse] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + in_process_course_actions_type_0 = [] + _in_process_course_actions_type_0 = data + for in_process_course_actions_type_0_item_data in _in_process_course_actions_type_0: + in_process_course_actions_type_0_item = UnsucceededCourse.from_dict( + in_process_course_actions_type_0_item_data + ) + + in_process_course_actions_type_0.append(in_process_course_actions_type_0_item) + + return in_process_course_actions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[UnsucceededCourse] | None | Unset, data) + + in_process_course_actions = _parse_in_process_course_actions(d.pop("in_process_course_actions", UNSET)) + + def _parse_libraries(data: object) -> list[LibraryView] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + libraries_type_0 = [] + _libraries_type_0 = data + for libraries_type_0_item_data in _libraries_type_0: + libraries_type_0_item = LibraryView.from_dict(libraries_type_0_item_data) + + libraries_type_0.append(libraries_type_0_item) + + return libraries_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[LibraryView] | None | Unset, data) + + libraries = _parse_libraries(d.pop("libraries", UNSET)) + + studio_home = cls( + allow_course_reruns=allow_course_reruns, + allow_to_create_new_org=allow_to_create_new_org, + allow_unicode_course_id=allow_unicode_course_id, + allowed_organizations=allowed_organizations, + allowed_organizations_for_libraries=allowed_organizations_for_libraries, + can_access_advanced_settings=can_access_advanced_settings, + can_create_organizations=can_create_organizations, + course_creator_status=course_creator_status, + libraries_enabled=libraries_enabled, + libraries_v1_enabled=libraries_v1_enabled, + libraries_v2_enabled=libraries_v2_enabled, + taxonomies_enabled=taxonomies_enabled, + taxonomy_list_mfe_url=taxonomy_list_mfe_url, + request_course_creator_url=request_course_creator_url, + rerun_creator_status=rerun_creator_status, + show_new_library_button=show_new_library_button, + show_new_library_v2_button=show_new_library_v2_button, + split_studio_home=split_studio_home, + studio_name=studio_name, + studio_short_name=studio_short_name, + studio_request_email=studio_request_email, + tech_support_email=tech_support_email, + platform_name=platform_name, + user_is_active=user_is_active, + archived_courses=archived_courses, + courses=courses, + in_process_course_actions=in_process_course_actions, + libraries=libraries, + ) + + studio_home.additional_properties = d + return studio_home + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/unsucceeded_course.py b/openedx_platform_sdk/models/unsucceeded_course.py new file mode 100644 index 0000000..bcc6552 --- /dev/null +++ b/openedx_platform_sdk/models/unsucceeded_course.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UnsucceededCourse") + + +@_attrs_define +class UnsucceededCourse: + """Serializer for unsucceeded course + + Attributes: + display_name (str): + course_key (str): + org (str): + number (str): + run (str): + is_failed (bool): + is_in_progress (bool): + dismiss_link (str): + """ + + display_name: str + course_key: str + org: str + number: str + run: str + is_failed: bool + is_in_progress: bool + dismiss_link: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + display_name = self.display_name + + course_key = self.course_key + + org = self.org + + number = self.number + + run = self.run + + is_failed = self.is_failed + + is_in_progress = self.is_in_progress + + dismiss_link = self.dismiss_link + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "display_name": display_name, + "course_key": course_key, + "org": org, + "number": number, + "run": run, + "is_failed": is_failed, + "is_in_progress": is_in_progress, + "dismiss_link": dismiss_link, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + display_name = d.pop("display_name") + + course_key = d.pop("course_key") + + org = d.pop("org") + + number = d.pop("number") + + run = d.pop("run") + + is_failed = d.pop("is_failed") + + is_in_progress = d.pop("is_in_progress") + + dismiss_link = d.pop("dismiss_link") + + unsucceeded_course = cls( + display_name=display_name, + course_key=course_key, + org=org, + number=number, + run=run, + is_failed=is_failed, + is_in_progress=is_in_progress, + dismiss_link=dismiss_link, + ) + + unsucceeded_course.additional_properties = d + return unsucceeded_course + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/unsucceeded_course_serializer_v2.py b/openedx_platform_sdk/models/unsucceeded_course_serializer_v2.py new file mode 100644 index 0000000..a258eca --- /dev/null +++ b/openedx_platform_sdk/models/unsucceeded_course_serializer_v2.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UnsucceededCourseSerializerV2") + + +@_attrs_define +class UnsucceededCourseSerializerV2: + """Serializer for unsucceeded course. + + Attributes: + display_name (str): + course_key (str): + org (str): + number (str): + run (str): + is_failed (bool): + is_in_progress (bool): + dismiss_link (str): + """ + + display_name: str + course_key: str + org: str + number: str + run: str + is_failed: bool + is_in_progress: bool + dismiss_link: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + display_name = self.display_name + + course_key = self.course_key + + org = self.org + + number = self.number + + run = self.run + + is_failed = self.is_failed + + is_in_progress = self.is_in_progress + + dismiss_link = self.dismiss_link + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "display_name": display_name, + "course_key": course_key, + "org": org, + "number": number, + "run": run, + "is_failed": is_failed, + "is_in_progress": is_in_progress, + "dismiss_link": dismiss_link, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + display_name = d.pop("display_name") + + course_key = d.pop("course_key") + + org = d.pop("org") + + number = d.pop("number") + + run = d.pop("run") + + is_failed = d.pop("is_failed") + + is_in_progress = d.pop("is_in_progress") + + dismiss_link = d.pop("dismiss_link") + + unsucceeded_course_serializer_v2 = cls( + display_name=display_name, + course_key=course_key, + org=org, + number=number, + run=run, + is_failed=is_failed, + is_in_progress=is_in_progress, + dismiss_link=dismiss_link, + ) + + unsucceeded_course_serializer_v2.additional_properties = d + return unsucceeded_course_serializer_v2 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/v3_course_details_retrieve_view.py b/openedx_platform_sdk/models/v3_course_details_retrieve_view.py new file mode 100644 index 0000000..c458af4 --- /dev/null +++ b/openedx_platform_sdk/models/v3_course_details_retrieve_view.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class V3CourseDetailsRetrieveView(str, Enum): + MINIMAL = "minimal" + + def __str__(self) -> str: + return str(self.value) diff --git a/openedx_platform_sdk/models/xblock.py b/openedx_platform_sdk/models/xblock.py new file mode 100644 index 0000000..9d83043 --- /dev/null +++ b/openedx_platform_sdk/models/xblock.py @@ -0,0 +1,1397 @@ +from __future__ import annotations + +import json +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..models.publish_enum import PublishEnum +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.xblock_actions_type_0 import XblockActionsType0 + from ..models.xblock_fields_type_0 import XblockFieldsType0 + from ..models.xblock_group_access_type_0 import XblockGroupAccessType0 + from ..models.xblock_metadata_type_0 import XblockMetadataType0 + from ..models.xblock_user_partition_info_type_0 import XblockUserPartitionInfoType0 + + +T = TypeVar("T", bound="Xblock") + + +@_attrs_define +class Xblock: + """A serializer for xblocks that enforces strict validation. + + The serializer ensures: + 1. All top-level fields have the expected data types. + 2. No unexpected fields are passed in. + + Note: The current list of fields is not exhaustive. It is primarily designed + to support the CMS API demo. While optional fields have been added, they were + chosen based on ease of discovery, not comprehensiveness. + + Attributes: + id (None | str | Unset): + parent_locator (None | str | Unset): + display_name (None | str | Unset): + category (None | str | Unset): + data (None | str | Unset): + metadata (None | Unset | XblockMetadataType0): + has_changes (bool | None | Unset): + children (list[Any] | None | Unset): + fields (None | Unset | XblockFieldsType0): + has_children (bool | None | Unset): + video_sharing_enabled (bool | None | Unset): + video_sharing_options (None | str | Unset): + video_sharing_doc_url (None | str | Unset): + edited_on (None | str | Unset): + published (bool | None | Unset): + published_on (Any | Unset): + studio_url (None | str | Unset): + released_to_students (bool | None | Unset): + release_date (Any | Unset): + nullout (Any | Unset): + grader_type (Any | Unset): + visibility_state (None | str | Unset): + has_explicit_staff_lock (bool | None | Unset): + start (None | str | Unset): + graded (bool | None | Unset): + due_date (None | str | Unset): + due (Any | Unset): + relative_weeks_due (Any | Unset): + format_ (Any | Unset): + course_graders (list[Any] | None | Unset): + actions (None | Unset | XblockActionsType0): + explanatory_message (None | str | Unset): + group_access (None | Unset | XblockGroupAccessType0): + user_partitions (list[Any] | None | Unset): + show_correctness (None | str | Unset): + discussion_enabled (bool | None | Unset): + ancestor_has_staff_lock (bool | None | Unset): + user_partition_info (None | Unset | XblockUserPartitionInfoType0): + summary_configuration_enabled (Any | Unset): + is_prereq (bool | None | Unset): + prereq_usage_key (None | str | Unset): + prereq_min_score (int | None | Unset): + prereq_min_completion (int | None | Unset): + publish (None | PublishEnum | Unset): + duplicate_source_locator (None | str | Unset): + move_source_locator (None | str | Unset): + target_index (int | None | Unset): + boilerplate (Any | Unset): + staged_content (None | str | Unset): + hide_from_toc (bool | None | Unset): + """ + + id: None | str | Unset = UNSET + parent_locator: None | str | Unset = UNSET + display_name: None | str | Unset = UNSET + category: None | str | Unset = UNSET + data: None | str | Unset = UNSET + metadata: None | Unset | XblockMetadataType0 = UNSET + has_changes: bool | None | Unset = UNSET + children: list[Any] | None | Unset = UNSET + fields: None | Unset | XblockFieldsType0 = UNSET + has_children: bool | None | Unset = UNSET + video_sharing_enabled: bool | None | Unset = UNSET + video_sharing_options: None | str | Unset = UNSET + video_sharing_doc_url: None | str | Unset = UNSET + edited_on: None | str | Unset = UNSET + published: bool | None | Unset = UNSET + published_on: Any | Unset = UNSET + studio_url: None | str | Unset = UNSET + released_to_students: bool | None | Unset = UNSET + release_date: Any | Unset = UNSET + nullout: Any | Unset = UNSET + grader_type: Any | Unset = UNSET + visibility_state: None | str | Unset = UNSET + has_explicit_staff_lock: bool | None | Unset = UNSET + start: None | str | Unset = UNSET + graded: bool | None | Unset = UNSET + due_date: None | str | Unset = UNSET + due: Any | Unset = UNSET + relative_weeks_due: Any | Unset = UNSET + format_: Any | Unset = UNSET + course_graders: list[Any] | None | Unset = UNSET + actions: None | Unset | XblockActionsType0 = UNSET + explanatory_message: None | str | Unset = UNSET + group_access: None | Unset | XblockGroupAccessType0 = UNSET + user_partitions: list[Any] | None | Unset = UNSET + show_correctness: None | str | Unset = UNSET + discussion_enabled: bool | None | Unset = UNSET + ancestor_has_staff_lock: bool | None | Unset = UNSET + user_partition_info: None | Unset | XblockUserPartitionInfoType0 = UNSET + summary_configuration_enabled: Any | Unset = UNSET + is_prereq: bool | None | Unset = UNSET + prereq_usage_key: None | str | Unset = UNSET + prereq_min_score: int | None | Unset = UNSET + prereq_min_completion: int | None | Unset = UNSET + publish: None | PublishEnum | Unset = UNSET + duplicate_source_locator: None | str | Unset = UNSET + move_source_locator: None | str | Unset = UNSET + target_index: int | None | Unset = UNSET + boilerplate: Any | Unset = UNSET + staged_content: None | str | Unset = UNSET + hide_from_toc: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.xblock_actions_type_0 import XblockActionsType0 + from ..models.xblock_fields_type_0 import XblockFieldsType0 + from ..models.xblock_group_access_type_0 import XblockGroupAccessType0 + from ..models.xblock_metadata_type_0 import XblockMetadataType0 + from ..models.xblock_user_partition_info_type_0 import XblockUserPartitionInfoType0 + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + parent_locator: None | str | Unset + if isinstance(self.parent_locator, Unset): + parent_locator = UNSET + else: + parent_locator = self.parent_locator + + display_name: None | str | Unset + if isinstance(self.display_name, Unset): + display_name = UNSET + else: + display_name = self.display_name + + category: None | str | Unset + if isinstance(self.category, Unset): + category = UNSET + else: + category = self.category + + data: None | str | Unset + if isinstance(self.data, Unset): + data = UNSET + else: + data = self.data + + metadata: dict[str, Any] | None | Unset + if isinstance(self.metadata, Unset): + metadata = UNSET + elif isinstance(self.metadata, XblockMetadataType0): + metadata = self.metadata.to_dict() + else: + metadata = self.metadata + + has_changes: bool | None | Unset + if isinstance(self.has_changes, Unset): + has_changes = UNSET + else: + has_changes = self.has_changes + + children: list[Any] | None | Unset + if isinstance(self.children, Unset): + children = UNSET + elif isinstance(self.children, list): + children = self.children + + else: + children = self.children + + fields: dict[str, Any] | None | Unset + if isinstance(self.fields, Unset): + fields = UNSET + elif isinstance(self.fields, XblockFieldsType0): + fields = self.fields.to_dict() + else: + fields = self.fields + + has_children: bool | None | Unset + if isinstance(self.has_children, Unset): + has_children = UNSET + else: + has_children = self.has_children + + video_sharing_enabled: bool | None | Unset + if isinstance(self.video_sharing_enabled, Unset): + video_sharing_enabled = UNSET + else: + video_sharing_enabled = self.video_sharing_enabled + + video_sharing_options: None | str | Unset + if isinstance(self.video_sharing_options, Unset): + video_sharing_options = UNSET + else: + video_sharing_options = self.video_sharing_options + + video_sharing_doc_url: None | str | Unset + if isinstance(self.video_sharing_doc_url, Unset): + video_sharing_doc_url = UNSET + else: + video_sharing_doc_url = self.video_sharing_doc_url + + edited_on: None | str | Unset + if isinstance(self.edited_on, Unset): + edited_on = UNSET + else: + edited_on = self.edited_on + + published: bool | None | Unset + if isinstance(self.published, Unset): + published = UNSET + else: + published = self.published + + published_on = self.published_on + + studio_url: None | str | Unset + if isinstance(self.studio_url, Unset): + studio_url = UNSET + else: + studio_url = self.studio_url + + released_to_students: bool | None | Unset + if isinstance(self.released_to_students, Unset): + released_to_students = UNSET + else: + released_to_students = self.released_to_students + + release_date = self.release_date + + nullout = self.nullout + + grader_type = self.grader_type + + visibility_state: None | str | Unset + if isinstance(self.visibility_state, Unset): + visibility_state = UNSET + else: + visibility_state = self.visibility_state + + has_explicit_staff_lock: bool | None | Unset + if isinstance(self.has_explicit_staff_lock, Unset): + has_explicit_staff_lock = UNSET + else: + has_explicit_staff_lock = self.has_explicit_staff_lock + + start: None | str | Unset + if isinstance(self.start, Unset): + start = UNSET + else: + start = self.start + + graded: bool | None | Unset + if isinstance(self.graded, Unset): + graded = UNSET + else: + graded = self.graded + + due_date: None | str | Unset + if isinstance(self.due_date, Unset): + due_date = UNSET + else: + due_date = self.due_date + + due = self.due + + relative_weeks_due = self.relative_weeks_due + + format_ = self.format_ + + course_graders: list[Any] | None | Unset + if isinstance(self.course_graders, Unset): + course_graders = UNSET + elif isinstance(self.course_graders, list): + course_graders = self.course_graders + + else: + course_graders = self.course_graders + + actions: dict[str, Any] | None | Unset + if isinstance(self.actions, Unset): + actions = UNSET + elif isinstance(self.actions, XblockActionsType0): + actions = self.actions.to_dict() + else: + actions = self.actions + + explanatory_message: None | str | Unset + if isinstance(self.explanatory_message, Unset): + explanatory_message = UNSET + else: + explanatory_message = self.explanatory_message + + group_access: dict[str, Any] | None | Unset + if isinstance(self.group_access, Unset): + group_access = UNSET + elif isinstance(self.group_access, XblockGroupAccessType0): + group_access = self.group_access.to_dict() + else: + group_access = self.group_access + + user_partitions: list[Any] | None | Unset + if isinstance(self.user_partitions, Unset): + user_partitions = UNSET + elif isinstance(self.user_partitions, list): + user_partitions = self.user_partitions + + else: + user_partitions = self.user_partitions + + show_correctness: None | str | Unset + if isinstance(self.show_correctness, Unset): + show_correctness = UNSET + else: + show_correctness = self.show_correctness + + discussion_enabled: bool | None | Unset + if isinstance(self.discussion_enabled, Unset): + discussion_enabled = UNSET + else: + discussion_enabled = self.discussion_enabled + + ancestor_has_staff_lock: bool | None | Unset + if isinstance(self.ancestor_has_staff_lock, Unset): + ancestor_has_staff_lock = UNSET + else: + ancestor_has_staff_lock = self.ancestor_has_staff_lock + + user_partition_info: dict[str, Any] | None | Unset + if isinstance(self.user_partition_info, Unset): + user_partition_info = UNSET + elif isinstance(self.user_partition_info, XblockUserPartitionInfoType0): + user_partition_info = self.user_partition_info.to_dict() + else: + user_partition_info = self.user_partition_info + + summary_configuration_enabled = self.summary_configuration_enabled + + is_prereq: bool | None | Unset + if isinstance(self.is_prereq, Unset): + is_prereq = UNSET + else: + is_prereq = self.is_prereq + + prereq_usage_key: None | str | Unset + if isinstance(self.prereq_usage_key, Unset): + prereq_usage_key = UNSET + else: + prereq_usage_key = self.prereq_usage_key + + prereq_min_score: int | None | Unset + if isinstance(self.prereq_min_score, Unset): + prereq_min_score = UNSET + else: + prereq_min_score = self.prereq_min_score + + prereq_min_completion: int | None | Unset + if isinstance(self.prereq_min_completion, Unset): + prereq_min_completion = UNSET + else: + prereq_min_completion = self.prereq_min_completion + + publish: None | str | Unset + if isinstance(self.publish, Unset): + publish = UNSET + elif isinstance(self.publish, PublishEnum): + publish = self.publish.value + else: + publish = self.publish + + duplicate_source_locator: None | str | Unset + if isinstance(self.duplicate_source_locator, Unset): + duplicate_source_locator = UNSET + else: + duplicate_source_locator = self.duplicate_source_locator + + move_source_locator: None | str | Unset + if isinstance(self.move_source_locator, Unset): + move_source_locator = UNSET + else: + move_source_locator = self.move_source_locator + + target_index: int | None | Unset + if isinstance(self.target_index, Unset): + target_index = UNSET + else: + target_index = self.target_index + + boilerplate = self.boilerplate + + staged_content: None | str | Unset + if isinstance(self.staged_content, Unset): + staged_content = UNSET + else: + staged_content = self.staged_content + + hide_from_toc: bool | None | Unset + if isinstance(self.hide_from_toc, Unset): + hide_from_toc = UNSET + else: + hide_from_toc = self.hide_from_toc + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if id is not UNSET: + field_dict["id"] = id + if parent_locator is not UNSET: + field_dict["parent_locator"] = parent_locator + if display_name is not UNSET: + field_dict["display_name"] = display_name + if category is not UNSET: + field_dict["category"] = category + if data is not UNSET: + field_dict["data"] = data + if metadata is not UNSET: + field_dict["metadata"] = metadata + if has_changes is not UNSET: + field_dict["has_changes"] = has_changes + if children is not UNSET: + field_dict["children"] = children + if fields is not UNSET: + field_dict["fields"] = fields + if has_children is not UNSET: + field_dict["has_children"] = has_children + if video_sharing_enabled is not UNSET: + field_dict["video_sharing_enabled"] = video_sharing_enabled + if video_sharing_options is not UNSET: + field_dict["video_sharing_options"] = video_sharing_options + if video_sharing_doc_url is not UNSET: + field_dict["video_sharing_doc_url"] = video_sharing_doc_url + if edited_on is not UNSET: + field_dict["edited_on"] = edited_on + if published is not UNSET: + field_dict["published"] = published + if published_on is not UNSET: + field_dict["published_on"] = published_on + if studio_url is not UNSET: + field_dict["studio_url"] = studio_url + if released_to_students is not UNSET: + field_dict["released_to_students"] = released_to_students + if release_date is not UNSET: + field_dict["release_date"] = release_date + if nullout is not UNSET: + field_dict["nullout"] = nullout + if grader_type is not UNSET: + field_dict["graderType"] = grader_type + if visibility_state is not UNSET: + field_dict["visibility_state"] = visibility_state + if has_explicit_staff_lock is not UNSET: + field_dict["has_explicit_staff_lock"] = has_explicit_staff_lock + if start is not UNSET: + field_dict["start"] = start + if graded is not UNSET: + field_dict["graded"] = graded + if due_date is not UNSET: + field_dict["due_date"] = due_date + if due is not UNSET: + field_dict["due"] = due + if relative_weeks_due is not UNSET: + field_dict["relative_weeks_due"] = relative_weeks_due + if format_ is not UNSET: + field_dict["format"] = format_ + if course_graders is not UNSET: + field_dict["course_graders"] = course_graders + if actions is not UNSET: + field_dict["actions"] = actions + if explanatory_message is not UNSET: + field_dict["explanatory_message"] = explanatory_message + if group_access is not UNSET: + field_dict["group_access"] = group_access + if user_partitions is not UNSET: + field_dict["user_partitions"] = user_partitions + if show_correctness is not UNSET: + field_dict["show_correctness"] = show_correctness + if discussion_enabled is not UNSET: + field_dict["discussion_enabled"] = discussion_enabled + if ancestor_has_staff_lock is not UNSET: + field_dict["ancestor_has_staff_lock"] = ancestor_has_staff_lock + if user_partition_info is not UNSET: + field_dict["user_partition_info"] = user_partition_info + if summary_configuration_enabled is not UNSET: + field_dict["summary_configuration_enabled"] = summary_configuration_enabled + if is_prereq is not UNSET: + field_dict["isPrereq"] = is_prereq + if prereq_usage_key is not UNSET: + field_dict["prereqUsageKey"] = prereq_usage_key + if prereq_min_score is not UNSET: + field_dict["prereqMinScore"] = prereq_min_score + if prereq_min_completion is not UNSET: + field_dict["prereqMinCompletion"] = prereq_min_completion + if publish is not UNSET: + field_dict["publish"] = publish + if duplicate_source_locator is not UNSET: + field_dict["duplicate_source_locator"] = duplicate_source_locator + if move_source_locator is not UNSET: + field_dict["move_source_locator"] = move_source_locator + if target_index is not UNSET: + field_dict["target_index"] = target_index + if boilerplate is not UNSET: + field_dict["boilerplate"] = boilerplate + if staged_content is not UNSET: + field_dict["staged_content"] = staged_content + if hide_from_toc is not UNSET: + field_dict["hide_from_toc"] = hide_from_toc + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + from ..models.xblock_actions_type_0 import XblockActionsType0 + from ..models.xblock_fields_type_0 import XblockFieldsType0 + from ..models.xblock_group_access_type_0 import XblockGroupAccessType0 + from ..models.xblock_metadata_type_0 import XblockMetadataType0 + from ..models.xblock_user_partition_info_type_0 import XblockUserPartitionInfoType0 + + files: types.RequestFiles = [] + + if not isinstance(self.id, Unset): + if isinstance(self.id, str): + files.append(("id", (None, str(self.id).encode(), "text/plain"))) + else: + files.append(("id", (None, str(self.id).encode(), "text/plain"))) + + if not isinstance(self.parent_locator, Unset): + if isinstance(self.parent_locator, str): + files.append(("parent_locator", (None, str(self.parent_locator).encode(), "text/plain"))) + else: + files.append(("parent_locator", (None, str(self.parent_locator).encode(), "text/plain"))) + + if not isinstance(self.display_name, Unset): + if isinstance(self.display_name, str): + files.append(("display_name", (None, str(self.display_name).encode(), "text/plain"))) + else: + files.append(("display_name", (None, str(self.display_name).encode(), "text/plain"))) + + if not isinstance(self.category, Unset): + if isinstance(self.category, str): + files.append(("category", (None, str(self.category).encode(), "text/plain"))) + else: + files.append(("category", (None, str(self.category).encode(), "text/plain"))) + + if not isinstance(self.data, Unset): + if isinstance(self.data, str): + files.append(("data", (None, str(self.data).encode(), "text/plain"))) + else: + files.append(("data", (None, str(self.data).encode(), "text/plain"))) + + if not isinstance(self.metadata, Unset): + if isinstance(self.metadata, XblockMetadataType0): + files.append(("metadata", (None, json.dumps(self.metadata.to_dict()).encode(), "application/json"))) + else: + files.append(("metadata", (None, str(self.metadata).encode(), "text/plain"))) + + if not isinstance(self.has_changes, Unset): + if isinstance(self.has_changes, bool): + files.append(("has_changes", (None, str(self.has_changes).encode(), "text/plain"))) + else: + files.append(("has_changes", (None, str(self.has_changes).encode(), "text/plain"))) + + if not isinstance(self.children, Unset): + if isinstance(self.children, list): + for children_type_0_item_element in self.children: + files.append(("children", (None, str(children_type_0_item_element).encode(), "text/plain"))) + else: + files.append(("children", (None, str(self.children).encode(), "text/plain"))) + + if not isinstance(self.fields, Unset): + if isinstance(self.fields, XblockFieldsType0): + files.append(("fields", (None, json.dumps(self.fields.to_dict()).encode(), "application/json"))) + else: + files.append(("fields", (None, str(self.fields).encode(), "text/plain"))) + + if not isinstance(self.has_children, Unset): + if isinstance(self.has_children, bool): + files.append(("has_children", (None, str(self.has_children).encode(), "text/plain"))) + else: + files.append(("has_children", (None, str(self.has_children).encode(), "text/plain"))) + + if not isinstance(self.video_sharing_enabled, Unset): + if isinstance(self.video_sharing_enabled, bool): + files.append(("video_sharing_enabled", (None, str(self.video_sharing_enabled).encode(), "text/plain"))) + else: + files.append(("video_sharing_enabled", (None, str(self.video_sharing_enabled).encode(), "text/plain"))) + + if not isinstance(self.video_sharing_options, Unset): + if isinstance(self.video_sharing_options, str): + files.append(("video_sharing_options", (None, str(self.video_sharing_options).encode(), "text/plain"))) + else: + files.append(("video_sharing_options", (None, str(self.video_sharing_options).encode(), "text/plain"))) + + if not isinstance(self.video_sharing_doc_url, Unset): + if isinstance(self.video_sharing_doc_url, str): + files.append(("video_sharing_doc_url", (None, str(self.video_sharing_doc_url).encode(), "text/plain"))) + else: + files.append(("video_sharing_doc_url", (None, str(self.video_sharing_doc_url).encode(), "text/plain"))) + + if not isinstance(self.edited_on, Unset): + if isinstance(self.edited_on, str): + files.append(("edited_on", (None, str(self.edited_on).encode(), "text/plain"))) + else: + files.append(("edited_on", (None, str(self.edited_on).encode(), "text/plain"))) + + if not isinstance(self.published, Unset): + if isinstance(self.published, bool): + files.append(("published", (None, str(self.published).encode(), "text/plain"))) + else: + files.append(("published", (None, str(self.published).encode(), "text/plain"))) + + if not isinstance(self.published_on, Unset): + files.append(("published_on", (None, str(self.published_on).encode(), "text/plain"))) + + if not isinstance(self.studio_url, Unset): + if isinstance(self.studio_url, str): + files.append(("studio_url", (None, str(self.studio_url).encode(), "text/plain"))) + else: + files.append(("studio_url", (None, str(self.studio_url).encode(), "text/plain"))) + + if not isinstance(self.released_to_students, Unset): + if isinstance(self.released_to_students, bool): + files.append(("released_to_students", (None, str(self.released_to_students).encode(), "text/plain"))) + else: + files.append(("released_to_students", (None, str(self.released_to_students).encode(), "text/plain"))) + + if not isinstance(self.release_date, Unset): + files.append(("release_date", (None, str(self.release_date).encode(), "text/plain"))) + + if not isinstance(self.nullout, Unset): + files.append(("nullout", (None, str(self.nullout).encode(), "text/plain"))) + + if not isinstance(self.grader_type, Unset): + files.append(("graderType", (None, str(self.grader_type).encode(), "text/plain"))) + + if not isinstance(self.visibility_state, Unset): + if isinstance(self.visibility_state, str): + files.append(("visibility_state", (None, str(self.visibility_state).encode(), "text/plain"))) + else: + files.append(("visibility_state", (None, str(self.visibility_state).encode(), "text/plain"))) + + if not isinstance(self.has_explicit_staff_lock, Unset): + if isinstance(self.has_explicit_staff_lock, bool): + files.append( + ("has_explicit_staff_lock", (None, str(self.has_explicit_staff_lock).encode(), "text/plain")) + ) + else: + files.append( + ("has_explicit_staff_lock", (None, str(self.has_explicit_staff_lock).encode(), "text/plain")) + ) + + if not isinstance(self.start, Unset): + if isinstance(self.start, str): + files.append(("start", (None, str(self.start).encode(), "text/plain"))) + else: + files.append(("start", (None, str(self.start).encode(), "text/plain"))) + + if not isinstance(self.graded, Unset): + if isinstance(self.graded, bool): + files.append(("graded", (None, str(self.graded).encode(), "text/plain"))) + else: + files.append(("graded", (None, str(self.graded).encode(), "text/plain"))) + + if not isinstance(self.due_date, Unset): + if isinstance(self.due_date, str): + files.append(("due_date", (None, str(self.due_date).encode(), "text/plain"))) + else: + files.append(("due_date", (None, str(self.due_date).encode(), "text/plain"))) + + if not isinstance(self.due, Unset): + files.append(("due", (None, str(self.due).encode(), "text/plain"))) + + if not isinstance(self.relative_weeks_due, Unset): + files.append(("relative_weeks_due", (None, str(self.relative_weeks_due).encode(), "text/plain"))) + + if not isinstance(self.format_, Unset): + files.append(("format", (None, str(self.format_).encode(), "text/plain"))) + + if not isinstance(self.course_graders, Unset): + if isinstance(self.course_graders, list): + for course_graders_type_0_item_element in self.course_graders: + files.append( + ("course_graders", (None, str(course_graders_type_0_item_element).encode(), "text/plain")) + ) + else: + files.append(("course_graders", (None, str(self.course_graders).encode(), "text/plain"))) + + if not isinstance(self.actions, Unset): + if isinstance(self.actions, XblockActionsType0): + files.append(("actions", (None, json.dumps(self.actions.to_dict()).encode(), "application/json"))) + else: + files.append(("actions", (None, str(self.actions).encode(), "text/plain"))) + + if not isinstance(self.explanatory_message, Unset): + if isinstance(self.explanatory_message, str): + files.append(("explanatory_message", (None, str(self.explanatory_message).encode(), "text/plain"))) + else: + files.append(("explanatory_message", (None, str(self.explanatory_message).encode(), "text/plain"))) + + if not isinstance(self.group_access, Unset): + if isinstance(self.group_access, XblockGroupAccessType0): + files.append( + ("group_access", (None, json.dumps(self.group_access.to_dict()).encode(), "application/json")) + ) + else: + files.append(("group_access", (None, str(self.group_access).encode(), "text/plain"))) + + if not isinstance(self.user_partitions, Unset): + if isinstance(self.user_partitions, list): + for user_partitions_type_0_item_element in self.user_partitions: + files.append( + ("user_partitions", (None, str(user_partitions_type_0_item_element).encode(), "text/plain")) + ) + else: + files.append(("user_partitions", (None, str(self.user_partitions).encode(), "text/plain"))) + + if not isinstance(self.show_correctness, Unset): + if isinstance(self.show_correctness, str): + files.append(("show_correctness", (None, str(self.show_correctness).encode(), "text/plain"))) + else: + files.append(("show_correctness", (None, str(self.show_correctness).encode(), "text/plain"))) + + if not isinstance(self.discussion_enabled, Unset): + if isinstance(self.discussion_enabled, bool): + files.append(("discussion_enabled", (None, str(self.discussion_enabled).encode(), "text/plain"))) + else: + files.append(("discussion_enabled", (None, str(self.discussion_enabled).encode(), "text/plain"))) + + if not isinstance(self.ancestor_has_staff_lock, Unset): + if isinstance(self.ancestor_has_staff_lock, bool): + files.append( + ("ancestor_has_staff_lock", (None, str(self.ancestor_has_staff_lock).encode(), "text/plain")) + ) + else: + files.append( + ("ancestor_has_staff_lock", (None, str(self.ancestor_has_staff_lock).encode(), "text/plain")) + ) + + if not isinstance(self.user_partition_info, Unset): + if isinstance(self.user_partition_info, XblockUserPartitionInfoType0): + files.append( + ( + "user_partition_info", + (None, json.dumps(self.user_partition_info.to_dict()).encode(), "application/json"), + ) + ) + else: + files.append(("user_partition_info", (None, str(self.user_partition_info).encode(), "text/plain"))) + + if not isinstance(self.summary_configuration_enabled, Unset): + files.append( + ( + "summary_configuration_enabled", + (None, str(self.summary_configuration_enabled).encode(), "text/plain"), + ) + ) + + if not isinstance(self.is_prereq, Unset): + if isinstance(self.is_prereq, bool): + files.append(("isPrereq", (None, str(self.is_prereq).encode(), "text/plain"))) + else: + files.append(("isPrereq", (None, str(self.is_prereq).encode(), "text/plain"))) + + if not isinstance(self.prereq_usage_key, Unset): + if isinstance(self.prereq_usage_key, str): + files.append(("prereqUsageKey", (None, str(self.prereq_usage_key).encode(), "text/plain"))) + else: + files.append(("prereqUsageKey", (None, str(self.prereq_usage_key).encode(), "text/plain"))) + + if not isinstance(self.prereq_min_score, Unset): + if isinstance(self.prereq_min_score, int): + files.append(("prereqMinScore", (None, str(self.prereq_min_score).encode(), "text/plain"))) + else: + files.append(("prereqMinScore", (None, str(self.prereq_min_score).encode(), "text/plain"))) + + if not isinstance(self.prereq_min_completion, Unset): + if isinstance(self.prereq_min_completion, int): + files.append(("prereqMinCompletion", (None, str(self.prereq_min_completion).encode(), "text/plain"))) + else: + files.append(("prereqMinCompletion", (None, str(self.prereq_min_completion).encode(), "text/plain"))) + + if not isinstance(self.publish, Unset): + if isinstance(self.publish, PublishEnum): + files.append(("publish", (None, str(self.publish.value).encode(), "text/plain"))) + else: + files.append(("publish", (None, str(self.publish).encode(), "text/plain"))) + + if not isinstance(self.duplicate_source_locator, Unset): + if isinstance(self.duplicate_source_locator, str): + files.append( + ("duplicate_source_locator", (None, str(self.duplicate_source_locator).encode(), "text/plain")) + ) + else: + files.append( + ("duplicate_source_locator", (None, str(self.duplicate_source_locator).encode(), "text/plain")) + ) + + if not isinstance(self.move_source_locator, Unset): + if isinstance(self.move_source_locator, str): + files.append(("move_source_locator", (None, str(self.move_source_locator).encode(), "text/plain"))) + else: + files.append(("move_source_locator", (None, str(self.move_source_locator).encode(), "text/plain"))) + + if not isinstance(self.target_index, Unset): + if isinstance(self.target_index, int): + files.append(("target_index", (None, str(self.target_index).encode(), "text/plain"))) + else: + files.append(("target_index", (None, str(self.target_index).encode(), "text/plain"))) + + if not isinstance(self.boilerplate, Unset): + files.append(("boilerplate", (None, str(self.boilerplate).encode(), "text/plain"))) + + if not isinstance(self.staged_content, Unset): + if isinstance(self.staged_content, str): + files.append(("staged_content", (None, str(self.staged_content).encode(), "text/plain"))) + else: + files.append(("staged_content", (None, str(self.staged_content).encode(), "text/plain"))) + + if not isinstance(self.hide_from_toc, Unset): + if isinstance(self.hide_from_toc, bool): + files.append(("hide_from_toc", (None, str(self.hide_from_toc).encode(), "text/plain"))) + else: + files.append(("hide_from_toc", (None, str(self.hide_from_toc).encode(), "text/plain"))) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.xblock_actions_type_0 import XblockActionsType0 + from ..models.xblock_fields_type_0 import XblockFieldsType0 + from ..models.xblock_group_access_type_0 import XblockGroupAccessType0 + from ..models.xblock_metadata_type_0 import XblockMetadataType0 + from ..models.xblock_user_partition_info_type_0 import XblockUserPartitionInfoType0 + + d = dict(src_dict) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + def _parse_parent_locator(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + parent_locator = _parse_parent_locator(d.pop("parent_locator", UNSET)) + + def _parse_display_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + display_name = _parse_display_name(d.pop("display_name", UNSET)) + + def _parse_category(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + category = _parse_category(d.pop("category", UNSET)) + + def _parse_data(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + data = _parse_data(d.pop("data", UNSET)) + + def _parse_metadata(data: object) -> None | Unset | XblockMetadataType0: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + metadata_type_0 = XblockMetadataType0.from_dict(data) + + return metadata_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | XblockMetadataType0, data) + + metadata = _parse_metadata(d.pop("metadata", UNSET)) + + def _parse_has_changes(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + has_changes = _parse_has_changes(d.pop("has_changes", UNSET)) + + def _parse_children(data: object) -> list[Any] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + children_type_0 = cast(list[Any], data) + + return children_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[Any] | None | Unset, data) + + children = _parse_children(d.pop("children", UNSET)) + + def _parse_fields(data: object) -> None | Unset | XblockFieldsType0: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fields_type_0 = XblockFieldsType0.from_dict(data) + + return fields_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | XblockFieldsType0, data) + + fields = _parse_fields(d.pop("fields", UNSET)) + + def _parse_has_children(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + has_children = _parse_has_children(d.pop("has_children", UNSET)) + + def _parse_video_sharing_enabled(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + video_sharing_enabled = _parse_video_sharing_enabled(d.pop("video_sharing_enabled", UNSET)) + + def _parse_video_sharing_options(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + video_sharing_options = _parse_video_sharing_options(d.pop("video_sharing_options", UNSET)) + + def _parse_video_sharing_doc_url(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + video_sharing_doc_url = _parse_video_sharing_doc_url(d.pop("video_sharing_doc_url", UNSET)) + + def _parse_edited_on(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + edited_on = _parse_edited_on(d.pop("edited_on", UNSET)) + + def _parse_published(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + published = _parse_published(d.pop("published", UNSET)) + + published_on = d.pop("published_on", UNSET) + + def _parse_studio_url(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + studio_url = _parse_studio_url(d.pop("studio_url", UNSET)) + + def _parse_released_to_students(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + released_to_students = _parse_released_to_students(d.pop("released_to_students", UNSET)) + + release_date = d.pop("release_date", UNSET) + + nullout = d.pop("nullout", UNSET) + + grader_type = d.pop("graderType", UNSET) + + def _parse_visibility_state(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + visibility_state = _parse_visibility_state(d.pop("visibility_state", UNSET)) + + def _parse_has_explicit_staff_lock(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + has_explicit_staff_lock = _parse_has_explicit_staff_lock(d.pop("has_explicit_staff_lock", UNSET)) + + def _parse_start(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + start = _parse_start(d.pop("start", UNSET)) + + def _parse_graded(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + graded = _parse_graded(d.pop("graded", UNSET)) + + def _parse_due_date(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + due_date = _parse_due_date(d.pop("due_date", UNSET)) + + due = d.pop("due", UNSET) + + relative_weeks_due = d.pop("relative_weeks_due", UNSET) + + format_ = d.pop("format", UNSET) + + def _parse_course_graders(data: object) -> list[Any] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + course_graders_type_0 = cast(list[Any], data) + + return course_graders_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[Any] | None | Unset, data) + + course_graders = _parse_course_graders(d.pop("course_graders", UNSET)) + + def _parse_actions(data: object) -> None | Unset | XblockActionsType0: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + actions_type_0 = XblockActionsType0.from_dict(data) + + return actions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | XblockActionsType0, data) + + actions = _parse_actions(d.pop("actions", UNSET)) + + def _parse_explanatory_message(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + explanatory_message = _parse_explanatory_message(d.pop("explanatory_message", UNSET)) + + def _parse_group_access(data: object) -> None | Unset | XblockGroupAccessType0: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + group_access_type_0 = XblockGroupAccessType0.from_dict(data) + + return group_access_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | XblockGroupAccessType0, data) + + group_access = _parse_group_access(d.pop("group_access", UNSET)) + + def _parse_user_partitions(data: object) -> list[Any] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + user_partitions_type_0 = cast(list[Any], data) + + return user_partitions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[Any] | None | Unset, data) + + user_partitions = _parse_user_partitions(d.pop("user_partitions", UNSET)) + + def _parse_show_correctness(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + show_correctness = _parse_show_correctness(d.pop("show_correctness", UNSET)) + + def _parse_discussion_enabled(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + discussion_enabled = _parse_discussion_enabled(d.pop("discussion_enabled", UNSET)) + + def _parse_ancestor_has_staff_lock(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + ancestor_has_staff_lock = _parse_ancestor_has_staff_lock(d.pop("ancestor_has_staff_lock", UNSET)) + + def _parse_user_partition_info(data: object) -> None | Unset | XblockUserPartitionInfoType0: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + user_partition_info_type_0 = XblockUserPartitionInfoType0.from_dict(data) + + return user_partition_info_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | XblockUserPartitionInfoType0, data) + + user_partition_info = _parse_user_partition_info(d.pop("user_partition_info", UNSET)) + + summary_configuration_enabled = d.pop("summary_configuration_enabled", UNSET) + + def _parse_is_prereq(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + is_prereq = _parse_is_prereq(d.pop("isPrereq", UNSET)) + + def _parse_prereq_usage_key(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + prereq_usage_key = _parse_prereq_usage_key(d.pop("prereqUsageKey", UNSET)) + + def _parse_prereq_min_score(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + prereq_min_score = _parse_prereq_min_score(d.pop("prereqMinScore", UNSET)) + + def _parse_prereq_min_completion(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + prereq_min_completion = _parse_prereq_min_completion(d.pop("prereqMinCompletion", UNSET)) + + def _parse_publish(data: object) -> None | PublishEnum | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + publish_type_0 = PublishEnum(data) + + return publish_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEnum | Unset, data) + + publish = _parse_publish(d.pop("publish", UNSET)) + + def _parse_duplicate_source_locator(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + duplicate_source_locator = _parse_duplicate_source_locator(d.pop("duplicate_source_locator", UNSET)) + + def _parse_move_source_locator(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + move_source_locator = _parse_move_source_locator(d.pop("move_source_locator", UNSET)) + + def _parse_target_index(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + target_index = _parse_target_index(d.pop("target_index", UNSET)) + + boilerplate = d.pop("boilerplate", UNSET) + + def _parse_staged_content(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + staged_content = _parse_staged_content(d.pop("staged_content", UNSET)) + + def _parse_hide_from_toc(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + hide_from_toc = _parse_hide_from_toc(d.pop("hide_from_toc", UNSET)) + + xblock = cls( + id=id, + parent_locator=parent_locator, + display_name=display_name, + category=category, + data=data, + metadata=metadata, + has_changes=has_changes, + children=children, + fields=fields, + has_children=has_children, + video_sharing_enabled=video_sharing_enabled, + video_sharing_options=video_sharing_options, + video_sharing_doc_url=video_sharing_doc_url, + edited_on=edited_on, + published=published, + published_on=published_on, + studio_url=studio_url, + released_to_students=released_to_students, + release_date=release_date, + nullout=nullout, + grader_type=grader_type, + visibility_state=visibility_state, + has_explicit_staff_lock=has_explicit_staff_lock, + start=start, + graded=graded, + due_date=due_date, + due=due, + relative_weeks_due=relative_weeks_due, + format_=format_, + course_graders=course_graders, + actions=actions, + explanatory_message=explanatory_message, + group_access=group_access, + user_partitions=user_partitions, + show_correctness=show_correctness, + discussion_enabled=discussion_enabled, + ancestor_has_staff_lock=ancestor_has_staff_lock, + user_partition_info=user_partition_info, + summary_configuration_enabled=summary_configuration_enabled, + is_prereq=is_prereq, + prereq_usage_key=prereq_usage_key, + prereq_min_score=prereq_min_score, + prereq_min_completion=prereq_min_completion, + publish=publish, + duplicate_source_locator=duplicate_source_locator, + move_source_locator=move_source_locator, + target_index=target_index, + boilerplate=boilerplate, + staged_content=staged_content, + hide_from_toc=hide_from_toc, + ) + + xblock.additional_properties = d + return xblock + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/xblock_actions_type_0.py b/openedx_platform_sdk/models/xblock_actions_type_0.py new file mode 100644 index 0000000..33eb9c6 --- /dev/null +++ b/openedx_platform_sdk/models/xblock_actions_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="XblockActionsType0") + + +@_attrs_define +class XblockActionsType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + xblock_actions_type_0 = cls() + + xblock_actions_type_0.additional_properties = d + return xblock_actions_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/xblock_fields_type_0.py b/openedx_platform_sdk/models/xblock_fields_type_0.py new file mode 100644 index 0000000..0abb493 --- /dev/null +++ b/openedx_platform_sdk/models/xblock_fields_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="XblockFieldsType0") + + +@_attrs_define +class XblockFieldsType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + xblock_fields_type_0 = cls() + + xblock_fields_type_0.additional_properties = d + return xblock_fields_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/xblock_group_access_type_0.py b/openedx_platform_sdk/models/xblock_group_access_type_0.py new file mode 100644 index 0000000..12a6c31 --- /dev/null +++ b/openedx_platform_sdk/models/xblock_group_access_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="XblockGroupAccessType0") + + +@_attrs_define +class XblockGroupAccessType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + xblock_group_access_type_0 = cls() + + xblock_group_access_type_0.additional_properties = d + return xblock_group_access_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/xblock_metadata_type_0.py b/openedx_platform_sdk/models/xblock_metadata_type_0.py new file mode 100644 index 0000000..2d21553 --- /dev/null +++ b/openedx_platform_sdk/models/xblock_metadata_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="XblockMetadataType0") + + +@_attrs_define +class XblockMetadataType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + xblock_metadata_type_0 = cls() + + xblock_metadata_type_0.additional_properties = d + return xblock_metadata_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/xblock_user_partition_info_type_0.py b/openedx_platform_sdk/models/xblock_user_partition_info_type_0.py new file mode 100644 index 0000000..4a4ba60 --- /dev/null +++ b/openedx_platform_sdk/models/xblock_user_partition_info_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="XblockUserPartitionInfoType0") + + +@_attrs_define +class XblockUserPartitionInfoType0: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + xblock_user_partition_info_type_0 = cls() + + xblock_user_partition_info_type_0.additional_properties = d + return xblock_user_partition_info_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/py.typed b/openedx_platform_sdk/py.typed new file mode 100644 index 0000000..1aad327 --- /dev/null +++ b/openedx_platform_sdk/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 \ No newline at end of file diff --git a/openedx_platform_sdk/types.py b/openedx_platform_sdk/types.py new file mode 100644 index 0000000..b64af09 --- /dev/null +++ b/openedx_platform_sdk/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0310c40 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "openedx-platform-sdk" +version = "0.1.0" +description = "A client library for accessing Authoring API" +authors = [] +readme = "README.md" +packages = [ + { include = "openedx_platform_sdk" }, +] +include = ["openedx_platform_sdk/py.typed"] + +[tool.poetry.dependencies] +python = "^3.11" +httpx = ">=0.23.1,<0.29.0" +attrs = ">=22.2.0" + +[tool.poetry.group.dev.dependencies] +openapi-python-client = ">=0.21.0" +pyyaml = ">=6.0" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["F", "I", "UP"] From a591fbb117febe6efaf9a901c469d146f7ac66dc Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Wed, 24 Jun 2026 15:14:34 +0500 Subject: [PATCH 3/8] feat: add OAuth2 client credentials auth helper Implements OAuth2ClientCredentials for the OpenedX JWT auth flow: - Fetches JWT via POST to {lms_url}/oauth2/access_token (client_credentials grant) - Caches token and auto-refreshes 60s before expiry - Returns AuthenticatedClient with prefix="JWT" (required by OpenedX) Usage: auth = OAuth2ClientCredentials(lms_url, client_id, client_secret) with auth.get_client(studio_url) as client: result = v3_home_list.sync(client=client) Co-Authored-By: Claude Sonnet 4.6 --- openedx_platform_sdk/__init__.py | 2 + openedx_platform_sdk/auth.py | 113 +++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 openedx_platform_sdk/auth.py diff --git a/openedx_platform_sdk/__init__.py b/openedx_platform_sdk/__init__.py index 11180d8..6caeaac 100644 --- a/openedx_platform_sdk/__init__.py +++ b/openedx_platform_sdk/__init__.py @@ -1,8 +1,10 @@ """A client library for accessing Authoring API""" +from .auth import OAuth2ClientCredentials from .client import AuthenticatedClient, Client __all__ = ( "AuthenticatedClient", "Client", + "OAuth2ClientCredentials", ) diff --git a/openedx_platform_sdk/auth.py b/openedx_platform_sdk/auth.py new file mode 100644 index 0000000..1073f05 --- /dev/null +++ b/openedx_platform_sdk/auth.py @@ -0,0 +1,113 @@ +""" +OAuth2 authentication helpers for the OpenedX Authoring SDK. + +OpenedX Studio uses JWT tokens obtained via the OAuth2 client_credentials flow. +The Authorization header must use the "JWT" prefix (not "Bearer"). + +Usage: + from openedx_platform_sdk.auth import OAuth2ClientCredentials + from openedx_platform_sdk import AuthenticatedClient + + auth = OAuth2ClientCredentials( + lms_url="http://localhost:18000", + client_id="your-client-id", + client_secret="your-client-secret", + ) + + client = auth.get_client(studio_url="http://localhost:18010") + + # Use with context manager (recommended) + with auth.get_client(studio_url="http://localhost:18010") as client: + result = some_api.sync(client=client) + + # Token auto-refreshes on the next get_client() call once expired. +""" + +import time +from typing import Optional + +import httpx + +from openedx_platform_sdk.client import AuthenticatedClient + + +class OAuth2ClientCredentials: + """ + Fetches and caches a JWT token from the OpenedX LMS OAuth2 endpoint. + Automatically refreshes when the token is within `refresh_buffer_seconds` of expiry. + """ + + TOKEN_ENDPOINT = "/oauth2/access_token" + + def __init__( + self, + lms_url: str, + client_id: str, + client_secret: str, + refresh_buffer_seconds: int = 60, + verify_ssl: bool = True, + ): + """ + Args: + lms_url: Base URL of the LMS (e.g. "http://localhost:18000") + client_id: OAuth2 client ID registered in the LMS + client_secret: OAuth2 client secret + refresh_buffer_seconds: Refresh the token this many seconds before it expires + verify_ssl: Whether to verify SSL certificates + """ + self.lms_url = lms_url.rstrip("/") + self.client_id = client_id + self.client_secret = client_secret + self.refresh_buffer_seconds = refresh_buffer_seconds + self.verify_ssl = verify_ssl + + self._token: Optional[str] = None + self._expires_at: float = 0.0 + + def _fetch_token(self) -> None: + response = httpx.post( + f"{self.lms_url}{self.TOKEN_ENDPOINT}", + data={ + "grant_type": "client_credentials", + "client_id": self.client_id, + "client_secret": self.client_secret, + "token_type": "jwt", + }, + verify=self.verify_ssl, + ) + response.raise_for_status() + data = response.json() + self._token = data["access_token"] + expires_in = data.get("expires_in", 3600) + self._expires_at = time.monotonic() + expires_in + + def get_token(self) -> str: + """Return a valid JWT token, fetching or refreshing as needed.""" + if self._token is None or time.monotonic() >= (self._expires_at - self.refresh_buffer_seconds): + self._fetch_token() + return self._token # type: ignore[return-value] + + def get_client( + self, + studio_url: str, + raise_on_unexpected_status: bool = False, + verify_ssl: Optional[bool] = None, + **kwargs, + ) -> AuthenticatedClient: + """ + Return an AuthenticatedClient pre-configured with a valid JWT token. + + Args: + studio_url: Base URL of Studio (e.g. "http://localhost:18010/api/contentstore") + raise_on_unexpected_status: Raise on undocumented status codes + verify_ssl: Override SSL verification for the Studio client + **kwargs: Additional arguments forwarded to AuthenticatedClient + """ + return AuthenticatedClient( + base_url=studio_url.rstrip("/"), + token=self.get_token(), + prefix="JWT", + raise_on_unexpected_status=raise_on_unexpected_status, + verify_ssl=verify_ssl if verify_ssl is not None else self.verify_ssl, + **kwargs, + ) From d064444ce0321436955b0c8b0706559695e41d5d Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Wed, 24 Jun 2026 15:14:42 +0500 Subject: [PATCH 4/8] feat: add regen_sdk.sh automation script Automates the full SDK regeneration workflow: 1. Optional branch checkout in openedx-platform 2. Download OpenAPI schema from running Studio (STUDIO_URL env var) 3. Filter schema to openedx-platform-sdk tagged paths via filter_schema.py 4. Run openapi-python-client update (or generate on first run) Usage: ./regen_sdk.sh # uses current platform branch ./regen_sdk.sh feat/axim-api_improvements # checkout branch first Co-Authored-By: Claude Sonnet 4.6 --- regen_sdk.sh | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100755 regen_sdk.sh diff --git a/regen_sdk.sh b/regen_sdk.sh new file mode 100755 index 0000000..e729e65 --- /dev/null +++ b/regen_sdk.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Regenerate openedx-platform-sdk from the openedx-platform schema. +# +# Usage: +# ./regen_sdk.sh # uses current branch in openedx-platform +# ./regen_sdk.sh feat/axim-api_improvements # checkout branch first, then regenerate +# +# Requirements: +# pip install openapi-python-client pyyaml +# Studio running at $STUDIO_URL (default: http://studio.local.openedx.io:8001) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLATFORM_DIR="$(dirname "$SCRIPT_DIR")/openedx-platform" +SCHEMA_FILE="$SCRIPT_DIR/schema.yml" +FILTERED_SCHEMA_FILE="$SCRIPT_DIR/filtered_schema.yml" +FILTER_SCRIPT="$SCRIPT_DIR/filter_schema.py" +CONFIG_FILE="$SCRIPT_DIR/config.yml" +STUDIO_URL="${STUDIO_URL:-http://studio.local.openedx.io:8001}" +SDK_TAG="openedx-platform-sdk" + +# ── 1. Optional branch checkout ─────────────────────────────────────────────── +BRANCH="${1:-}" +if [[ -n "$BRANCH" ]]; then + echo "→ Checking out branch '$BRANCH' in openedx-platform..." + git -C "$PLATFORM_DIR" fetch origin + git -C "$PLATFORM_DIR" checkout "$BRANCH" + echo " Current branch: $(git -C "$PLATFORM_DIR" branch --show-current)" +fi + +# ── 2. Download schema from running Studio ──────────────────────────────────── +echo "→ Downloading OpenAPI schema from $STUDIO_URL ..." +curl -sf "$STUDIO_URL/authoring-api/schema/" -o "$SCHEMA_FILE" +echo " Schema written to: $SCHEMA_FILE" + +# ── 3. Filter schema to SDK-tagged paths only ───────────────────────────────── +echo "→ Filtering schema to tag '$SDK_TAG'..." +python "$FILTER_SCRIPT" "$SCHEMA_FILE" "$FILTERED_SCHEMA_FILE" "$SDK_TAG" + +# ── 4. Regenerate the SDK ───────────────────────────────────────────────────── +echo "→ Regenerating SDK..." +cd "$SCRIPT_DIR" + +if [[ -d "$SCRIPT_DIR/openedx_platform_sdk" ]]; then + openapi-python-client update \ + --path "$FILTERED_SCHEMA_FILE" \ + --config "$CONFIG_FILE" +else + openapi-python-client generate \ + --path "$FILTERED_SCHEMA_FILE" \ + --config "$CONFIG_FILE" +fi + +echo "" +echo "✓ SDK regenerated successfully." +echo " Package : openedx_platform_sdk" +echo " Schema : $FILTERED_SCHEMA_FILE" +[[ -n "$BRANCH" ]] && echo " Branch : $BRANCH" From ad6a6ef6c6b59cef19786d273d406a6b1e2ae0dc Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Wed, 24 Jun 2026 16:48:30 +0500 Subject: [PATCH 5/8] docs: add testing steps & fix regen_sdk.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'Testing Locally' section to README: install, create OAuth2 app in LMS admin (user must be linked for JWT issuance), test script - Fix studio_url in auth example to include /api/contentstore prefix - Make regen_sdk.sh portable: PLATFORM_DIR env var for custom platform path, clear error if branch requested but repo not found, STUDIO_URL error message if Studio unreachable - Rename v3_home_list → v3_home_retrieve: fixed HomeViewSet list action schema to return a single StudioHome object (not array) by overriding _is_list_view in a custom AutoSchema; generator renamed the module accordingly Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 + README.md | 61 +++++++++++++- openedx_platform_sdk/__init__.py | 2 +- .../{v3_home_list.py => v3_home_retrieve.py} | 82 ++++++++++++++----- regen_sdk.sh | 65 +++++++++++---- 5 files changed, 172 insertions(+), 42 deletions(-) rename openedx_platform_sdk/api/openedx_platform_sdk/{v3_home_list.py => v3_home_retrieve.py} (70%) diff --git a/.gitignore b/.gitignore index 79a2c3d..2f61a09 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ dmypy.json /coverage.xml /.coverage + +# Generated by regen_sdk.sh — not committed +/schema.yml +/filtered_schema.yml diff --git a/README.md b/README.md index 3bb0f73..582b939 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,18 @@ openapi-python-client generate \ --config config.yml ``` -Or use the provided script (supports optional branch checkout): +Or use the provided script: ```bash -./regen_sdk.sh # uses current platform branch -./regen_sdk.sh feat/axim-api_improvements # checkout branch first, then regenerate +# Regenerate from a running Studio (no platform repo needed) +./regen_sdk.sh + +# Checkout a specific branch first, then regenerate +# PLATFORM_DIR defaults to ../openedx-platform — override if your checkout is elsewhere +PLATFORM_DIR=/path/to/openedx-platform ./regen_sdk.sh feat/axim-api_improvements + +# Use a different Studio URL +STUDIO_URL=http://studio.example.com:8001 ./regen_sdk.sh ``` --- @@ -66,7 +73,7 @@ auth = OAuth2ClientCredentials( ) # Get a ready-to-use authenticated client (token fetched automatically) -with auth.get_client(studio_url="http://localhost:18010/api/contentstore") as client: +with auth.get_client(studio_url="http://studio.local.openedx.io:8001/api/contentstore") as client: from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list result = v3_home_list.sync(client=client) ``` @@ -137,6 +144,52 @@ with client as client: --- +## Testing Locally + +### Prerequisites + +- A running [devstack](https://github.com/openedx/devstack) or Tutor instance +- Default URLs: LMS at `http://local.openedx.io:8000`, Studio at `http://studio.local.openedx.io:8001` + +### 1. Install the SDK + +```bash +cd openedx-platform-sdk +pip install -e . +``` + +### 2. Create an OAuth2 application in LMS + +1. Go to `http://local.openedx.io:8000/admin/oauth2_provider/application/` +2. Click **Add Application** +3. Fill in: + - **User**: any staff/admin user (required for JWT issuance) + - **Client type**: Confidential + - **Authorization grant type**: Client credentials + - **Name**: `openedx-platform-sdk` +4. Save and copy the generated **Client ID** and **Client Secret** + +### 3. Run a quick test + +```python +from openedx_platform_sdk import OAuth2ClientCredentials + +auth = OAuth2ClientCredentials( + lms_url="http://local.openedx.io:8000", + client_id="your-client-id", + client_secret="your-client-secret", +) + +with auth.get_client(studio_url="http://studio.local.openedx.io:8001/api/contentstore") as client: + r = client.get_httpx_client().request("GET", "/v3/home/") + print(r.status_code) # 200 + print(r.json()) +``` + +> **Note:** The `studio_url` must include `/api/contentstore` — the SDK appends versioned paths (e.g. `/v3/home/`) directly to this base. + +--- + ## Installation ```bash diff --git a/openedx_platform_sdk/__init__.py b/openedx_platform_sdk/__init__.py index 6caeaac..d326682 100644 --- a/openedx_platform_sdk/__init__.py +++ b/openedx_platform_sdk/__init__.py @@ -5,6 +5,6 @@ __all__ = ( "AuthenticatedClient", - "Client", "OAuth2ClientCredentials", + "Client", ) diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_list.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_retrieve.py similarity index 70% rename from openedx_platform_sdk/api/openedx_platform_sdk/v3_home_list.py rename to openedx_platform_sdk/api/openedx_platform_sdk/v3_home_retrieve.py index 6ad8065..ebd28da 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_list.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_home_retrieve.py @@ -6,27 +6,35 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.studio_home import StudioHome -from ...types import Response +from ...types import UNSET, Response, Unset -def _get_kwargs() -> dict[str, Any]: +def _get_kwargs( + *, + fields: str | Unset = UNSET, + org: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["fields"] = fields + + params["org"] = org + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} _kwargs: dict[str, Any] = { "method": "get", "url": "/v3/home/", + "params": params, } return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> list[StudioHome] | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> StudioHome | None: if response.status_code == 200: - response_200 = [] - _response_200 = response.json() - for response_200_item_data in _response_200: - response_200_item = StudioHome.from_dict(response_200_item_data) - - response_200.append(response_200_item) + response_200 = StudioHome.from_dict(response.json()) return response_200 @@ -36,7 +44,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[list[StudioHome]]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[StudioHome]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -48,7 +56,9 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( *, client: AuthenticatedClient | Client, -) -> Response[list[StudioHome]]: + fields: str | Unset = UNSET, + org: str | Unset = UNSET, +) -> Response[StudioHome]: """Get an object containing all courses and libraries on home page. **Example Request** @@ -56,15 +66,22 @@ def sync_detailed( GET /api/contentstore/v3/home/ GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + Args: + fields (str | Unset): + org (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[list[StudioHome]] + Response[StudioHome] """ - kwargs = _get_kwargs() + kwargs = _get_kwargs( + fields=fields, + org=org, + ) response = client.get_httpx_client().request( **kwargs, @@ -76,7 +93,9 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, -) -> list[StudioHome] | None: + fields: str | Unset = UNSET, + org: str | Unset = UNSET, +) -> StudioHome | None: """Get an object containing all courses and libraries on home page. **Example Request** @@ -84,23 +103,31 @@ def sync( GET /api/contentstore/v3/home/ GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + Args: + fields (str | Unset): + org (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - list[StudioHome] + StudioHome """ return sync_detailed( client=client, + fields=fields, + org=org, ).parsed async def asyncio_detailed( *, client: AuthenticatedClient | Client, -) -> Response[list[StudioHome]]: + fields: str | Unset = UNSET, + org: str | Unset = UNSET, +) -> Response[StudioHome]: """Get an object containing all courses and libraries on home page. **Example Request** @@ -108,15 +135,22 @@ async def asyncio_detailed( GET /api/contentstore/v3/home/ GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + Args: + fields (str | Unset): + org (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[list[StudioHome]] + Response[StudioHome] """ - kwargs = _get_kwargs() + kwargs = _get_kwargs( + fields=fields, + org=org, + ) response = await client.get_async_httpx_client().request(**kwargs) @@ -126,7 +160,9 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, -) -> list[StudioHome] | None: + fields: str | Unset = UNSET, + org: str | Unset = UNSET, +) -> StudioHome | None: """Get an object containing all courses and libraries on home page. **Example Request** @@ -134,16 +170,22 @@ async def asyncio( GET /api/contentstore/v3/home/ GET /api/contentstore/v3/home/?fields=courses,libraries (ADR 0036) + Args: + fields (str | Unset): + org (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - list[StudioHome] + StudioHome """ return ( await asyncio_detailed( client=client, + fields=fields, + org=org, ) ).parsed diff --git a/regen_sdk.sh b/regen_sdk.sh index e729e65..c73d6fc 100755 --- a/regen_sdk.sh +++ b/regen_sdk.sh @@ -1,29 +1,39 @@ #!/usr/bin/env bash -# Regenerate openedx-platform-sdk from the openedx-platform schema. +# Regenerate openedx-platform-sdk from the openedx-platform OpenAPI schema. # # Usage: -# ./regen_sdk.sh # uses current branch in openedx-platform -# ./regen_sdk.sh feat/axim-api_improvements # checkout branch first, then regenerate +# ./regen_sdk.sh # regenerate from running Studio (no platform repo needed) +# ./regen_sdk.sh feat/axim-api_improvements # checkout branch first, then regenerate +# +# Environment variables: +# STUDIO_URL Base URL of running Studio (default: http://studio.local.openedx.io:8001) +# PLATFORM_DIR Path to local openedx-platform repo (default: ../openedx-platform) +# Only needed when passing a branch argument. # # Requirements: # pip install openapi-python-client pyyaml -# Studio running at $STUDIO_URL (default: http://studio.local.openedx.io:8001) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PLATFORM_DIR="$(dirname "$SCRIPT_DIR")/openedx-platform" SCHEMA_FILE="$SCRIPT_DIR/schema.yml" FILTERED_SCHEMA_FILE="$SCRIPT_DIR/filtered_schema.yml" FILTER_SCRIPT="$SCRIPT_DIR/filter_schema.py" CONFIG_FILE="$SCRIPT_DIR/config.yml" STUDIO_URL="${STUDIO_URL:-http://studio.local.openedx.io:8001}" +PLATFORM_DIR="${PLATFORM_DIR:-$(dirname "$SCRIPT_DIR")/openedx-platform}" SDK_TAG="openedx-platform-sdk" # ── 1. Optional branch checkout ─────────────────────────────────────────────── BRANCH="${1:-}" if [[ -n "$BRANCH" ]]; then - echo "→ Checking out branch '$BRANCH' in openedx-platform..." + if [[ ! -d "$PLATFORM_DIR/.git" ]]; then + echo "Error: PLATFORM_DIR='$PLATFORM_DIR' is not a git repository." + echo "Set the PLATFORM_DIR env var to your local openedx-platform checkout:" + echo " PLATFORM_DIR=/path/to/openedx-platform ./regen_sdk.sh $BRANCH" + exit 1 + fi + echo "→ Checking out branch '$BRANCH' in $PLATFORM_DIR ..." git -C "$PLATFORM_DIR" fetch origin git -C "$PLATFORM_DIR" checkout "$BRANCH" echo " Current branch: $(git -C "$PLATFORM_DIR" branch --show-current)" @@ -31,7 +41,12 @@ fi # ── 2. Download schema from running Studio ──────────────────────────────────── echo "→ Downloading OpenAPI schema from $STUDIO_URL ..." -curl -sf "$STUDIO_URL/authoring-api/schema/" -o "$SCHEMA_FILE" +if ! curl -sf "$STUDIO_URL/authoring-api/schema/" -o "$SCHEMA_FILE"; then + echo "Error: Could not reach $STUDIO_URL/authoring-api/schema/" + echo "Make sure Studio is running. Override the URL with:" + echo " STUDIO_URL=http://your-studio-url:port ./regen_sdk.sh" + exit 1 +fi echo " Schema written to: $SCHEMA_FILE" # ── 3. Filter schema to SDK-tagged paths only ───────────────────────────────── @@ -40,16 +55,32 @@ python "$FILTER_SCRIPT" "$SCHEMA_FILE" "$FILTERED_SCHEMA_FILE" "$SDK_TAG" # ── 4. Regenerate the SDK ───────────────────────────────────────────────────── echo "→ Regenerating SDK..." -cd "$SCRIPT_DIR" - -if [[ -d "$SCRIPT_DIR/openedx_platform_sdk" ]]; then - openapi-python-client update \ - --path "$FILTERED_SCHEMA_FILE" \ - --config "$CONFIG_FILE" -else - openapi-python-client generate \ - --path "$FILTERED_SCHEMA_FILE" \ - --config "$CONFIG_FILE" + +# Preserve hand-written files before wiping the package directory. +cp "$SCRIPT_DIR/openedx_platform_sdk/auth.py" /tmp/_sdk_auth.py 2>/dev/null || true + +# Generate into a temp directory; openapi-python-client always creates a new +# project folder — we only want the inner package directory. +TMPDIR="$(mktemp -d)" +openapi-python-client generate \ + --path "$FILTERED_SCHEMA_FILE" \ + --config "$CONFIG_FILE" \ + --output-path "$TMPDIR" \ + --overwrite + +# Replace only the auto-generated package directory +rm -rf "$SCRIPT_DIR/openedx_platform_sdk" +mv "$TMPDIR/openedx_platform_sdk" "$SCRIPT_DIR/openedx_platform_sdk" +rm -rf "$TMPDIR" + +# Restore hand-written files +if [[ -f /tmp/_sdk_auth.py ]]; then + cp /tmp/_sdk_auth.py "$SCRIPT_DIR/openedx_platform_sdk/auth.py" + # Re-add the auth export to __init__.py (openapi-python-client regenerates it without it) + sed -i '' 's/^from \.client import/from .auth import OAuth2ClientCredentials\nfrom .client import/' \ + "$SCRIPT_DIR/openedx_platform_sdk/__init__.py" + sed -i '' 's/"AuthenticatedClient",/"AuthenticatedClient",\n "OAuth2ClientCredentials",/' \ + "$SCRIPT_DIR/openedx_platform_sdk/__init__.py" fi echo "" From fddde6f34bd8d25a807b98d0d7f7847b3ace9e85 Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Fri, 3 Jul 2026 16:06:14 +0500 Subject: [PATCH 6/8] feat: regenerate SDK with grading schema fix, v4 paginated response, and generator bug fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerated from updated openedx-platform schemas. Three platform-side fixes drive this regen (see companion commit on add_changes_wrt_sdk): - Grading: grade_cutoffs, grace_period, minimum_grade_credit are now typed fields on AuthoringGradingCourseGradingV0 and PatchedauthoringGradingCourseGradingV0. Callers no longer need to pass these via additional_properties["key"] = value workarounds. New models: AuthoringGradingGracePeriodV0, *GradeCutoffs (additionalProperties wrapper for the dict field). - v4 Home: operation ID changed from v4_home_courses_list to v4_home_courses_retrieve (the schema now correctly describes a paginated object, not an array). New model PaginatedV4HomeCoursesResponse with count, num_pages, current_page, start, next_, previous, results. - Course details: certificate_available_date is now nullable in the schema; CourseDetails model updated accordingly. Generator bug fixes baked into regen_sdk.sh (applied after every regen): - Bug 1: v3_course_details_update.py uses Unset in type annotations but the generator only imports UNSET. Fixed with a sed post-step. - Bug 2: _get_kwargs emits three identical isinstance(body, X) blocks for json / form / multipart — the multipart block always wins and breaks nested-dict payloads. Removed data/multipart blocks, kept only the JSON block. Affects: v1_xblock_{create,update,partial_update}, v3_authoring_grading_partial_update, v3_course_details_update. - Bug 3: DictField wrapper models (e.g. GradeCutoffs) have their to_dict() called unconditionally in parent model's to_dict(), but users naturally pass plain Python dicts. Fixed with an isinstance guard: call .to_dict() only when the value is not already a dict. auth.py: set Accept: application/json as a default header in get_client(). XBlock retrieve returns 406 without it — httpx sends Accept: */* by default which the view rejects. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 29 ++-- .../openedx_platform_sdk/v1_xblock_create.py | 130 ++++++--------- .../openedx_platform_sdk/v1_xblock_destroy.py | 46 ++--- .../v1_xblock_partial_update.py | 124 ++++++-------- .../v1_xblock_retrieve.py | 157 ++++++++++-------- .../openedx_platform_sdk/v1_xblock_update.py | 120 +++++-------- .../v3_authoring_grading_partial_update.py | 10 -- .../v3_course_details_update.py | 10 +- ...es_list.py => v4_home_courses_retrieve.py} | 29 ++-- openedx_platform_sdk/auth.py | 3 + openedx_platform_sdk/models/__init__.py | 10 ++ .../authoring_grading_course_grading_v0.py | 78 +++++++++ ...grading_course_grading_v0_grade_cutoffs.py | 50 ++++++ .../authoring_grading_grace_period_v0.py | 80 +++++++++ openedx_platform_sdk/models/course_details.py | 39 ++++- .../models/course_home_tab.py | 45 ++--- .../paginated_v4_home_courses_response.py | 128 ++++++++++++++ ...chedauthoring_grading_course_grading_v0.py | 97 +++++++++++ ...grading_course_grading_v0_grade_cutoffs.py | 50 ++++++ .../models/v1_xblock_retrieve_view.py | 8 + regen_sdk.sh | 95 +++++++++++ 21 files changed, 930 insertions(+), 408 deletions(-) rename openedx_platform_sdk/api/openedx_platform_sdk/{v4_home_courses_list.py => v4_home_courses_retrieve.py} (89%) create mode 100644 openedx_platform_sdk/models/authoring_grading_course_grading_v0_grade_cutoffs.py create mode 100644 openedx_platform_sdk/models/authoring_grading_grace_period_v0.py create mode 100644 openedx_platform_sdk/models/paginated_v4_home_courses_response.py create mode 100644 openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0_grade_cutoffs.py create mode 100644 openedx_platform_sdk/models/v1_xblock_retrieve_view.py diff --git a/README.md b/README.md index 582b939..f20c8b8 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ auth = OAuth2ClientCredentials( # Get a ready-to-use authenticated client (token fetched automatically) with auth.get_client(studio_url="http://studio.local.openedx.io:8001/api/contentstore") as client: - from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list - result = v3_home_list.sync(client=client) + from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_retrieve + result = v3_home_retrieve.sync(client=client) ``` Token is cached and auto-refreshed 60 seconds before expiry. The `JWT` prefix is used automatically (required by OpenedX). @@ -99,13 +99,13 @@ client = AuthenticatedClient( ### Call an endpoint ```python -from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list +from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_retrieve from openedx_platform_sdk.api.openedx_platform_sdk import v3_course_details_retrieve -from openedx_platform_sdk.api.openedx_platform_sdk import v4_home_courses_list +from openedx_platform_sdk.api.openedx_platform_sdk import v4_home_courses_retrieve with client as client: # Get studio home - home = v3_home_list.sync(client=client) + home = v3_home_retrieve.sync(client=client) # Get course details details = v3_course_details_retrieve.sync( @@ -114,18 +114,18 @@ with client as client: ) # Get paginated courses (v4) - courses = v4_home_courses_list.sync(client=client, page=1, page_size=20) + courses = v4_home_courses_retrieve.sync(client=client) ``` ### Async support ```python import asyncio -from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list +from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_retrieve async def main(): async with client as c: - home = await v3_home_list.asyncio(client=c) + home = await v3_home_retrieve.asyncio(client=c) asyncio.run(main()) ``` @@ -133,11 +133,11 @@ asyncio.run(main()) ### Detailed response (status code, headers) ```python -from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_list +from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_retrieve from openedx_platform_sdk.types import Response with client as client: - response: Response = v3_home_list.sync_detailed(client=client) + response: Response = v3_home_retrieve.sync_detailed(client=client) print(response.status_code) print(response.parsed) ``` @@ -173,6 +173,7 @@ pip install -e . ```python from openedx_platform_sdk import OAuth2ClientCredentials +from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_retrieve auth = OAuth2ClientCredentials( lms_url="http://local.openedx.io:8000", @@ -181,13 +182,15 @@ auth = OAuth2ClientCredentials( ) with auth.get_client(studio_url="http://studio.local.openedx.io:8001/api/contentstore") as client: - r = client.get_httpx_client().request("GET", "/v3/home/") - print(r.status_code) # 200 - print(r.json()) + home = v3_home_retrieve.sync(client=client) + print(home.studio_name) + print(home.courses) ``` > **Note:** The `studio_url` must include `/api/contentstore` — the SDK appends versioned paths (e.g. `/v3/home/`) directly to this base. +For typed usage examples covering all API groups (Home v3/v4, Course Details, Authoring Grading, XBlock lifecycle), see **[docs/testing-sdk-apis.rst](docs/testing-sdk-apis.rst)**. + --- ## Installation diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py index 84dd59e..26db954 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_create.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Any +from typing import Any, cast import httpx @@ -25,25 +25,35 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() headers["Content-Type"] = "application/json" - if isinstance(body, Xblock): - if not isinstance(body, Unset): - _kwargs["data"] = body.to_dict() - headers["Content-Type"] = "application/x-www-form-urlencoded" - if isinstance(body, Xblock): - if not isinstance(body, Unset): - _kwargs["files"] = body.to_multipart() - - headers["Content-Type"] = "multipart/form-data; boundary=+++" - _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: - if response.status_code == 201: - response_201 = Xblock.from_dict(response.json()) +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | Xblock | None: + if response.status_code == 200: + response_200 = Xblock.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 - return response_201 + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if response.status_code == 406: + response_406 = cast(Any, None) + return response_406 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) @@ -51,7 +61,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | Xblock]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -64,21 +74,11 @@ def sync_detailed( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Response[Any | Xblock]: + """Create an xblock + + Create a new xblock under a parent block. The ``parent_locator`` field on the request body + identifies the parent and (implicitly) the course. Args: body (Xblock | Unset): A serializer for xblocks that enforces strict validation. @@ -114,7 +114,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( @@ -132,21 +132,11 @@ def sync( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Any | Xblock | None: + """Create an xblock + + Create a new xblock under a parent block. The ``parent_locator`` field on the request body + identifies the parent and (implicitly) the course. Args: body (Xblock | Unset): A serializer for xblocks that enforces strict validation. @@ -182,7 +172,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return sync_detailed( @@ -195,21 +185,11 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Response[Any | Xblock]: + """Create an xblock + + Create a new xblock under a parent block. The ``parent_locator`` field on the request body + identifies the parent and (implicitly) the course. Args: body (Xblock | Unset): A serializer for xblocks that enforces strict validation. @@ -245,7 +225,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( @@ -261,21 +241,11 @@ async def asyncio( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Any | Xblock | None: + """Create an xblock + + Create a new xblock under a parent block. The ``parent_locator`` field on the request body + identifies the parent and (implicitly) the course. Args: body (Xblock | Unset): A serializer for xblocks that enforces strict validation. @@ -311,7 +281,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return ( diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py index fb54437..47d9527 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_destroy.py @@ -24,7 +24,19 @@ def _get_kwargs( def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: - if response.status_code == 204: + if response.status_code == 200: + return None + + if response.status_code == 401: + return None + + if response.status_code == 403: + return None + + if response.status_code == 404: + return None + + if response.status_code == 406: return None if client.raise_on_unexpected_status: @@ -47,20 +59,9 @@ def sync_detailed( *, client: AuthenticatedClient | Client, ) -> Response[Any]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). + """Delete an xblock - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. + Delete an xblock identified by its usage key. Args: usage_key_string (str): @@ -89,20 +90,9 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, ) -> Response[Any]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. + """Delete an xblock + + Delete an xblock identified by its usage key. Args: usage_key_string (str): diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py index 26cdfe7..56a3cfa 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_partial_update.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Any +from typing import Any, cast from urllib.parse import quote import httpx @@ -30,33 +30,43 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() headers["Content-Type"] = "application/json" - if isinstance(body, PatchedXblock): - if not isinstance(body, Unset): - _kwargs["data"] = body.to_dict() - headers["Content-Type"] = "application/x-www-form-urlencoded" - if isinstance(body, PatchedXblock): - if not isinstance(body, Unset): - _kwargs["files"] = body.to_multipart() - - headers["Content-Type"] = "multipart/form-data; boundary=+++" - _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | Xblock | None: if response.status_code == 200: response_200 = Xblock.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if response.status_code == 406: + response_406 = cast(Any, None) + return response_406 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | Xblock]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -70,21 +80,11 @@ def sync_detailed( *, client: AuthenticatedClient | Client, body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Response[Any | Xblock]: + """Partially update an xblock + + Partially update an xblock identified by its usage key. Only the fields present in the request body + are updated. Args: usage_key_string (str): @@ -121,7 +121,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( @@ -141,21 +141,11 @@ def sync( *, client: AuthenticatedClient | Client, body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Any | Xblock | None: + """Partially update an xblock + + Partially update an xblock identified by its usage key. Only the fields present in the request body + are updated. Args: usage_key_string (str): @@ -192,7 +182,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return sync_detailed( @@ -207,21 +197,11 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Response[Any | Xblock]: + """Partially update an xblock + + Partially update an xblock identified by its usage key. Only the fields present in the request body + are updated. Args: usage_key_string (str): @@ -258,7 +238,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( @@ -276,21 +256,11 @@ async def asyncio( *, client: AuthenticatedClient | Client, body: PatchedXblock | PatchedXblock | PatchedXblock | Unset = UNSET, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Any | Xblock | None: + """Partially update an xblock + + Partially update an xblock identified by its usage key. Only the fields present in the request body + are updated. Args: usage_key_string (str): @@ -327,7 +297,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return ( diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py index 16aa531..bb81840 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_retrieve.py @@ -1,42 +1,75 @@ from http import HTTPStatus -from typing import Any +from typing import Any, cast from urllib.parse import quote import httpx from ... import errors from ...client import AuthenticatedClient, Client +from ...models.v1_xblock_retrieve_view import V1XblockRetrieveView from ...models.xblock import Xblock -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( usage_key_string: str, + *, + fields: str | Unset = UNSET, + view: V1XblockRetrieveView | Unset = UNSET, ) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["fields"] = fields + + json_view: str | Unset = UNSET + if not isinstance(view, Unset): + json_view = view.value + + params["view"] = json_view + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + _kwargs: dict[str, Any] = { "method": "get", "url": "/v1/xblock/{usage_key_string}/".format( usage_key_string=quote(str(usage_key_string), safe=""), ), + "params": params, } return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | Xblock | None: if response.status_code == 200: response_200 = Xblock.from_dict(response.json()) return response_200 + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if response.status_code == 406: + response_406 = cast(Any, None) + return response_406 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | Xblock]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,35 +82,32 @@ def sync_detailed( usage_key_string: str, *, client: AuthenticatedClient | Client, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. + fields: str | Unset = UNSET, + view: V1XblockRetrieveView | Unset = UNSET, +) -> Response[Any | Xblock]: + """Retrieve an xblock + + Retrieve an xblock (and, by default, its nested tree) by usage key. Supports ADR 0036 + ``?view=minimal`` to strip contextual fields, plus the legacy ``?fields=`` type-of-response + selector. Args: usage_key_string (str): + fields (str | Unset): + view (V1XblockRetrieveView | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( usage_key_string=usage_key_string, + fields=fields, + view=view, ) response = client.get_httpx_client().request( @@ -91,36 +121,33 @@ def sync( usage_key_string: str, *, client: AuthenticatedClient | Client, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. + fields: str | Unset = UNSET, + view: V1XblockRetrieveView | Unset = UNSET, +) -> Any | Xblock | None: + """Retrieve an xblock + + Retrieve an xblock (and, by default, its nested tree) by usage key. Supports ADR 0036 + ``?view=minimal`` to strip contextual fields, plus the legacy ``?fields=`` type-of-response + selector. Args: usage_key_string (str): + fields (str | Unset): + view (V1XblockRetrieveView | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return sync_detailed( usage_key_string=usage_key_string, client=client, + fields=fields, + view=view, ).parsed @@ -128,35 +155,32 @@ async def asyncio_detailed( usage_key_string: str, *, client: AuthenticatedClient | Client, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. + fields: str | Unset = UNSET, + view: V1XblockRetrieveView | Unset = UNSET, +) -> Response[Any | Xblock]: + """Retrieve an xblock + + Retrieve an xblock (and, by default, its nested tree) by usage key. Supports ADR 0036 + ``?view=minimal`` to strip contextual fields, plus the legacy ``?fields=`` type-of-response + selector. Args: usage_key_string (str): + fields (str | Unset): + view (V1XblockRetrieveView | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( usage_key_string=usage_key_string, + fields=fields, + view=view, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -168,36 +192,33 @@ async def asyncio( usage_key_string: str, *, client: AuthenticatedClient | Client, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. + fields: str | Unset = UNSET, + view: V1XblockRetrieveView | Unset = UNSET, +) -> Any | Xblock | None: + """Retrieve an xblock + + Retrieve an xblock (and, by default, its nested tree) by usage key. Supports ADR 0036 + ``?view=minimal`` to strip contextual fields, plus the legacy ``?fields=`` type-of-response + selector. Args: usage_key_string (str): + fields (str | Unset): + view (V1XblockRetrieveView | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return ( await asyncio_detailed( usage_key_string=usage_key_string, client=client, + fields=fields, + view=view, ) ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py index 394aa51..61be1cb 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v1_xblock_update.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Any +from typing import Any, cast from urllib.parse import quote import httpx @@ -29,33 +29,43 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() headers["Content-Type"] = "application/json" - if isinstance(body, Xblock): - if not isinstance(body, Unset): - _kwargs["data"] = body.to_dict() - headers["Content-Type"] = "application/x-www-form-urlencoded" - if isinstance(body, Xblock): - if not isinstance(body, Unset): - _kwargs["files"] = body.to_multipart() - - headers["Content-Type"] = "multipart/form-data; boundary=+++" - _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Xblock | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | Xblock | None: if response.status_code == 200: response_200 = Xblock.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if response.status_code == 406: + response_406 = cast(Any, None) + return response_406 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Xblock]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | Xblock]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -69,21 +79,10 @@ def sync_detailed( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Response[Any | Xblock]: + """Update an xblock + + Fully update an xblock identified by its usage key. Args: usage_key_string (str): @@ -120,7 +119,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( @@ -140,21 +139,10 @@ def sync( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Any | Xblock | None: + """Update an xblock + + Fully update an xblock identified by its usage key. Args: usage_key_string (str): @@ -191,7 +179,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return sync_detailed( @@ -206,21 +194,10 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Response[Xblock]: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Response[Any | Xblock]: + """Update an xblock + + Fully update an xblock identified by its usage key. Args: usage_key_string (str): @@ -257,7 +234,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Xblock] + Response[Any | Xblock] """ kwargs = _get_kwargs( @@ -275,21 +252,10 @@ async def asyncio( *, client: AuthenticatedClient | Client, body: Xblock | Xblock | Xblock | Unset = UNSET, -) -> Xblock | None: - """ViewSet for xblock CRUD operations (v1 — ADR 0028). - - Router-generated URLs: - POST /api/contentstore/v1/xblock/ → create - GET /api/contentstore/v1/xblock/{usage_key_string}/ → retrieve - PUT /api/contentstore/v1/xblock/{usage_key_string}/ → update - PATCH /api/contentstore/v1/xblock/{usage_key_string}/ → partial_update - DELETE /api/contentstore/v1/xblock/{usage_key_string}/ → destroy - - Query parameters (ADR 0036, GET only): - ?view=minimal Drop heavy / contextual fields from the response, - keeping only structural fields (id, display_name, - category, children, has_children, studio_url). - Default response is the full xblock payload. +) -> Any | Xblock | None: + """Update an xblock + + Fully update an xblock identified by its usage key. Args: usage_key_string (str): @@ -326,7 +292,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Xblock + Any | Xblock """ return ( diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py index dfbeb86..bbc31b3 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_authoring_grading_partial_update.py @@ -33,16 +33,6 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() headers["Content-Type"] = "application/json" - if isinstance(body, PatchedauthoringGradingCourseGradingV0): - if not isinstance(body, Unset): - _kwargs["data"] = body.to_dict() - headers["Content-Type"] = "application/x-www-form-urlencoded" - if isinstance(body, PatchedauthoringGradingCourseGradingV0): - if not isinstance(body, Unset): - _kwargs["files"] = body.to_multipart() - - headers["Content-Type"] = "multipart/form-data; boundary=+++" - _kwargs["headers"] = headers return _kwargs diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py b/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py index 7dddd62..f9f555d 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v3_course_details_update.py @@ -7,7 +7,7 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.course_details import CourseDetails -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( @@ -28,14 +28,6 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() headers["Content-Type"] = "application/json" - if isinstance(body, CourseDetails): - _kwargs["data"] = body.to_dict() - headers["Content-Type"] = "application/x-www-form-urlencoded" - if isinstance(body, CourseDetails): - _kwargs["files"] = body.to_multipart() - - headers["Content-Type"] = "multipart/form-data; boundary=+++" - _kwargs["headers"] = headers return _kwargs diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_list.py b/openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_retrieve.py similarity index 89% rename from openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_list.py rename to openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_retrieve.py index c60afa4..516f974 100644 --- a/openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_list.py +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v4_home_courses_retrieve.py @@ -5,7 +5,7 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.course_home_tab_serializer_v2 import CourseHomeTabSerializerV2 +from ...models.paginated_v4_home_courses_response import PaginatedV4HomeCoursesResponse from ...types import UNSET, Response, Unset @@ -52,14 +52,9 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Any | list[CourseHomeTabSerializerV2] | None: +) -> Any | PaginatedV4HomeCoursesResponse | None: if response.status_code == 200: - response_200 = [] - _response_200 = response.json() - for response_200_item_data in _response_200: - response_200_item = CourseHomeTabSerializerV2.from_dict(response_200_item_data) - - response_200.append(response_200_item) + response_200 = PaginatedV4HomeCoursesResponse.from_dict(response.json()) return response_200 @@ -75,7 +70,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[Any | list[CourseHomeTabSerializerV2]]: +) -> Response[Any | PaginatedV4HomeCoursesResponse]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -95,7 +90,7 @@ def sync_detailed( page: str | Unset = UNSET, page_size: str | Unset = UNSET, search: str | Unset = UNSET, -) -> Response[Any | list[CourseHomeTabSerializerV2]]: +) -> Response[Any | PaginatedV4HomeCoursesResponse]: """List courses for the Studio home page (paginated) Returns a paginated list of all courses available to the logged-in user, with optional filtering and @@ -116,7 +111,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Any | list[CourseHomeTabSerializerV2]] + Response[Any | PaginatedV4HomeCoursesResponse] """ kwargs = _get_kwargs( @@ -148,7 +143,7 @@ def sync( page: str | Unset = UNSET, page_size: str | Unset = UNSET, search: str | Unset = UNSET, -) -> Any | list[CourseHomeTabSerializerV2] | None: +) -> Any | PaginatedV4HomeCoursesResponse | None: """List courses for the Studio home page (paginated) Returns a paginated list of all courses available to the logged-in user, with optional filtering and @@ -169,7 +164,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Any | list[CourseHomeTabSerializerV2] + Any | PaginatedV4HomeCoursesResponse """ return sync_detailed( @@ -196,7 +191,7 @@ async def asyncio_detailed( page: str | Unset = UNSET, page_size: str | Unset = UNSET, search: str | Unset = UNSET, -) -> Response[Any | list[CourseHomeTabSerializerV2]]: +) -> Response[Any | PaginatedV4HomeCoursesResponse]: """List courses for the Studio home page (paginated) Returns a paginated list of all courses available to the logged-in user, with optional filtering and @@ -217,7 +212,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Any | list[CourseHomeTabSerializerV2]] + Response[Any | PaginatedV4HomeCoursesResponse] """ kwargs = _get_kwargs( @@ -247,7 +242,7 @@ async def asyncio( page: str | Unset = UNSET, page_size: str | Unset = UNSET, search: str | Unset = UNSET, -) -> Any | list[CourseHomeTabSerializerV2] | None: +) -> Any | PaginatedV4HomeCoursesResponse | None: """List courses for the Studio home page (paginated) Returns a paginated list of all courses available to the logged-in user, with optional filtering and @@ -268,7 +263,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Any | list[CourseHomeTabSerializerV2] + Any | PaginatedV4HomeCoursesResponse """ return ( diff --git a/openedx_platform_sdk/auth.py b/openedx_platform_sdk/auth.py index 1073f05..fe003d6 100644 --- a/openedx_platform_sdk/auth.py +++ b/openedx_platform_sdk/auth.py @@ -103,11 +103,14 @@ def get_client( verify_ssl: Override SSL verification for the Studio client **kwargs: Additional arguments forwarded to AuthenticatedClient """ + headers = kwargs.pop("headers", {}) + headers.setdefault("Accept", "application/json") return AuthenticatedClient( base_url=studio_url.rstrip("/"), token=self.get_token(), prefix="JWT", raise_on_unexpected_status=raise_on_unexpected_status, verify_ssl=verify_ssl if verify_ssl is not None else self.verify_ssl, + headers=headers, **kwargs, ) diff --git a/openedx_platform_sdk/models/__init__.py b/openedx_platform_sdk/models/__init__.py index 003193f..5d518fa 100644 --- a/openedx_platform_sdk/models/__init__.py +++ b/openedx_platform_sdk/models/__init__.py @@ -1,6 +1,8 @@ """Contains all the data models used in inputs/outputs""" from .authoring_grading_course_grading_v0 import AuthoringGradingCourseGradingV0 +from .authoring_grading_course_grading_v0_grade_cutoffs import AuthoringGradingCourseGradingV0GradeCutoffs +from .authoring_grading_grace_period_v0 import AuthoringGradingGracePeriodV0 from .authoring_grading_graders_v0 import AuthoringGradingGradersV0 from .course_common import CourseCommon from .course_common_serializer_v2 import CourseCommonSerializerV2 @@ -11,6 +13,7 @@ from .instructors import Instructors from .library_tab import LibraryTab from .library_view import LibraryView +from .paginated_v4_home_courses_response import PaginatedV4HomeCoursesResponse from .patched_xblock import PatchedXblock from .patched_xblock_actions_type_0 import PatchedXblockActionsType0 from .patched_xblock_fields_type_0 import PatchedXblockFieldsType0 @@ -18,10 +21,12 @@ from .patched_xblock_metadata_type_0 import PatchedXblockMetadataType0 from .patched_xblock_user_partition_info_type_0 import PatchedXblockUserPartitionInfoType0 from .patchedauthoring_grading_course_grading_v0 import PatchedauthoringGradingCourseGradingV0 +from .patchedauthoring_grading_course_grading_v0_grade_cutoffs import PatchedauthoringGradingCourseGradingV0GradeCutoffs from .publish_enum import PublishEnum from .studio_home import StudioHome from .unsucceeded_course import UnsucceededCourse from .unsucceeded_course_serializer_v2 import UnsucceededCourseSerializerV2 +from .v1_xblock_retrieve_view import V1XblockRetrieveView from .v3_course_details_retrieve_view import V3CourseDetailsRetrieveView from .xblock import Xblock from .xblock_actions_type_0 import XblockActionsType0 @@ -32,6 +37,8 @@ __all__ = ( "AuthoringGradingCourseGradingV0", + "AuthoringGradingCourseGradingV0GradeCutoffs", + "AuthoringGradingGracePeriodV0", "AuthoringGradingGradersV0", "CourseCommon", "CourseCommonSerializerV2", @@ -42,7 +49,9 @@ "Instructors", "LibraryTab", "LibraryView", + "PaginatedV4HomeCoursesResponse", "PatchedauthoringGradingCourseGradingV0", + "PatchedauthoringGradingCourseGradingV0GradeCutoffs", "PatchedXblock", "PatchedXblockActionsType0", "PatchedXblockFieldsType0", @@ -53,6 +62,7 @@ "StudioHome", "UnsucceededCourse", "UnsucceededCourseSerializerV2", + "V1XblockRetrieveView", "V3CourseDetailsRetrieveView", "Xblock", "XblockActionsType0", diff --git a/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py b/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py index 6c5538c..57846a8 100644 --- a/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py +++ b/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py @@ -6,7 +6,11 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..types import UNSET, Unset + if TYPE_CHECKING: + from ..models.authoring_grading_course_grading_v0_grade_cutoffs import AuthoringGradingCourseGradingV0GradeCutoffs + from ..models.authoring_grading_grace_period_v0 import AuthoringGradingGracePeriodV0 from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 @@ -19,12 +23,22 @@ class AuthoringGradingCourseGradingV0: Attributes: graders (list[AuthoringGradingGradersV0] | None): + grade_cutoffs (AuthoringGradingCourseGradingV0GradeCutoffs | Unset): Mapping of letter grade to minimum score + (0.0–1.0). Required by CourseGradingModel.update_from_json — must be included in every PATCH. + grace_period (AuthoringGradingGracePeriodV0 | None | Unset): Grace period duration. Pass null to clear the grace + period. + minimum_grade_credit (float | None | Unset): Minimum passing score for credit eligibility (0.0–1.0). """ graders: list[AuthoringGradingGradersV0] | None + grade_cutoffs: AuthoringGradingCourseGradingV0GradeCutoffs | Unset = UNSET + grace_period: AuthoringGradingGracePeriodV0 | None | Unset = UNSET + minimum_grade_credit: float | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.authoring_grading_grace_period_v0 import AuthoringGradingGracePeriodV0 + graders: list[dict[str, Any]] | None if isinstance(self.graders, list): graders = [] @@ -35,6 +49,24 @@ def to_dict(self) -> dict[str, Any]: else: graders = self.graders + grade_cutoffs: dict[str, Any] | Unset = UNSET + if not isinstance(self.grade_cutoffs, Unset): + grade_cutoffs = self.grade_cutoffs.to_dict() + + grace_period: dict[str, Any] | None | Unset + if isinstance(self.grace_period, Unset): + grace_period = UNSET + elif isinstance(self.grace_period, AuthoringGradingGracePeriodV0): + grace_period = self.grace_period.to_dict() + else: + grace_period = self.grace_period + + minimum_grade_credit: float | None | Unset + if isinstance(self.minimum_grade_credit, Unset): + minimum_grade_credit = UNSET + else: + minimum_grade_credit = self.minimum_grade_credit + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -42,11 +74,21 @@ def to_dict(self) -> dict[str, Any]: "graders": graders, } ) + if grade_cutoffs is not UNSET: + field_dict["grade_cutoffs"] = grade_cutoffs + if grace_period is not UNSET: + field_dict["grace_period"] = grace_period + if minimum_grade_credit is not UNSET: + field_dict["minimum_grade_credit"] = minimum_grade_credit return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.authoring_grading_course_grading_v0_grade_cutoffs import ( + AuthoringGradingCourseGradingV0GradeCutoffs, + ) + from ..models.authoring_grading_grace_period_v0 import AuthoringGradingGracePeriodV0 from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 d = dict(src_dict) @@ -71,8 +113,44 @@ def _parse_graders(data: object) -> list[AuthoringGradingGradersV0] | None: graders = _parse_graders(d.pop("graders")) + _grade_cutoffs = d.pop("grade_cutoffs", UNSET) + grade_cutoffs: AuthoringGradingCourseGradingV0GradeCutoffs | Unset + if isinstance(_grade_cutoffs, Unset): + grade_cutoffs = UNSET + else: + grade_cutoffs = AuthoringGradingCourseGradingV0GradeCutoffs.from_dict(_grade_cutoffs) + + def _parse_grace_period(data: object) -> AuthoringGradingGracePeriodV0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + grace_period_type_1 = AuthoringGradingGracePeriodV0.from_dict(data) + + return grace_period_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AuthoringGradingGracePeriodV0 | None | Unset, data) + + grace_period = _parse_grace_period(d.pop("grace_period", UNSET)) + + def _parse_minimum_grade_credit(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + minimum_grade_credit = _parse_minimum_grade_credit(d.pop("minimum_grade_credit", UNSET)) + authoring_grading_course_grading_v0 = cls( graders=graders, + grade_cutoffs=grade_cutoffs, + grace_period=grace_period, + minimum_grade_credit=minimum_grade_credit, ) authoring_grading_course_grading_v0.additional_properties = d diff --git a/openedx_platform_sdk/models/authoring_grading_course_grading_v0_grade_cutoffs.py b/openedx_platform_sdk/models/authoring_grading_course_grading_v0_grade_cutoffs.py new file mode 100644 index 0000000..f451e5a --- /dev/null +++ b/openedx_platform_sdk/models/authoring_grading_course_grading_v0_grade_cutoffs.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="AuthoringGradingCourseGradingV0GradeCutoffs") + + +@_attrs_define +class AuthoringGradingCourseGradingV0GradeCutoffs: + """Mapping of letter grade to minimum score (0.0–1.0). Required by CourseGradingModel.update_from_json — must be + included in every PATCH. + + """ + + additional_properties: dict[str, float] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + authoring_grading_course_grading_v0_grade_cutoffs = cls() + + authoring_grading_course_grading_v0_grade_cutoffs.additional_properties = d + return authoring_grading_course_grading_v0_grade_cutoffs + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> float: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: float) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/authoring_grading_grace_period_v0.py b/openedx_platform_sdk/models/authoring_grading_grace_period_v0.py new file mode 100644 index 0000000..bebc6db --- /dev/null +++ b/openedx_platform_sdk/models/authoring_grading_grace_period_v0.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AuthoringGradingGracePeriodV0") + + +@_attrs_define +class AuthoringGradingGracePeriodV0: + """Serializer for grace period (hours / minutes / seconds). + + Attributes: + hours (int | Unset): Default: 0. + minutes (int | Unset): Default: 0. + seconds (int | Unset): Default: 0. + """ + + hours: int | Unset = 0 + minutes: int | Unset = 0 + seconds: int | Unset = 0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + hours = self.hours + + minutes = self.minutes + + seconds = self.seconds + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if hours is not UNSET: + field_dict["hours"] = hours + if minutes is not UNSET: + field_dict["minutes"] = minutes + if seconds is not UNSET: + field_dict["seconds"] = seconds + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + hours = d.pop("hours", UNSET) + + minutes = d.pop("minutes", UNSET) + + seconds = d.pop("seconds", UNSET) + + authoring_grading_grace_period_v0 = cls( + hours=hours, + minutes=minutes, + seconds=seconds, + ) + + authoring_grading_grace_period_v0.additional_properties = d + return authoring_grading_grace_period_v0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_details.py b/openedx_platform_sdk/models/course_details.py index 7d0ab72..9c22cab 100644 --- a/openedx_platform_sdk/models/course_details.py +++ b/openedx_platform_sdk/models/course_details.py @@ -25,7 +25,7 @@ class CourseDetails: about_sidebar_html (None | str): banner_image_name (str): banner_image_asset_path (str): - certificate_available_date (datetime.datetime): + certificate_available_date (datetime.datetime | None): certificates_display_behavior (None | str): course_id (str): course_image_asset_path (str): @@ -62,7 +62,7 @@ class CourseDetails: about_sidebar_html: None | str banner_image_name: str banner_image_asset_path: str - certificate_available_date: datetime.datetime + certificate_available_date: datetime.datetime | None certificates_display_behavior: None | str course_id: str course_image_asset_path: str @@ -104,7 +104,11 @@ def to_dict(self) -> dict[str, Any]: banner_image_asset_path = self.banner_image_asset_path - certificate_available_date = self.certificate_available_date.isoformat() + certificate_available_date: None | str + if isinstance(self.certificate_available_date, datetime.datetime): + certificate_available_date = self.certificate_available_date.isoformat() + else: + certificate_available_date = self.certificate_available_date certificates_display_behavior: None | str certificates_display_behavior = self.certificates_display_behavior @@ -242,9 +246,17 @@ def to_multipart(self) -> types.RequestFiles: files.append(("banner_image_asset_path", (None, str(self.banner_image_asset_path).encode(), "text/plain"))) - files.append( - ("certificate_available_date", (None, self.certificate_available_date.isoformat().encode(), "text/plain")) - ) + if isinstance(self.certificate_available_date, datetime.datetime): + files.append( + ( + "certificate_available_date", + (None, self.certificate_available_date.isoformat().encode(), "text/plain"), + ) + ) + else: + files.append( + ("certificate_available_date", (None, str(self.certificate_available_date).encode(), "text/plain")) + ) if isinstance(self.certificates_display_behavior, str): files.append( @@ -385,7 +397,20 @@ def _parse_about_sidebar_html(data: object) -> None | str: banner_image_asset_path = d.pop("banner_image_asset_path") - certificate_available_date = datetime.datetime.fromisoformat(d.pop("certificate_available_date")) + def _parse_certificate_available_date(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + certificate_available_date_type_0 = datetime.datetime.fromisoformat(data) + + return certificate_available_date_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + certificate_available_date = _parse_certificate_available_date(d.pop("certificate_available_date")) def _parse_certificates_display_behavior(data: object) -> None | str: if data is None: diff --git a/openedx_platform_sdk/models/course_home_tab.py b/openedx_platform_sdk/models/course_home_tab.py index aab8a45..801dfa1 100644 --- a/openedx_platform_sdk/models/course_home_tab.py +++ b/openedx_platform_sdk/models/course_home_tab.py @@ -18,26 +18,20 @@ @_attrs_define class CourseHomeTab: - """ + """Serializer for the courses tab of the Studio home page. + Attributes: - archived_courses (list[CourseCommon] | Unset): courses (list[CourseCommon] | Unset): + archived_courses (list[CourseCommon] | Unset): in_process_course_actions (list[UnsucceededCourse] | None | Unset): """ - archived_courses: list[CourseCommon] | Unset = UNSET courses: list[CourseCommon] | Unset = UNSET + archived_courses: list[CourseCommon] | Unset = UNSET in_process_course_actions: list[UnsucceededCourse] | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - archived_courses: list[dict[str, Any]] | Unset = UNSET - if not isinstance(self.archived_courses, Unset): - archived_courses = [] - for archived_courses_item_data in self.archived_courses: - archived_courses_item = archived_courses_item_data.to_dict() - archived_courses.append(archived_courses_item) - courses: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.courses, Unset): courses = [] @@ -45,6 +39,13 @@ def to_dict(self) -> dict[str, Any]: courses_item = courses_item_data.to_dict() courses.append(courses_item) + archived_courses: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.archived_courses, Unset): + archived_courses = [] + for archived_courses_item_data in self.archived_courses: + archived_courses_item = archived_courses_item_data.to_dict() + archived_courses.append(archived_courses_item) + in_process_course_actions: list[dict[str, Any]] | None | Unset if isinstance(self.in_process_course_actions, Unset): in_process_course_actions = UNSET @@ -60,10 +61,10 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) - if archived_courses is not UNSET: - field_dict["archived_courses"] = archived_courses if courses is not UNSET: field_dict["courses"] = courses + if archived_courses is not UNSET: + field_dict["archived_courses"] = archived_courses if in_process_course_actions is not UNSET: field_dict["in_process_course_actions"] = in_process_course_actions @@ -75,15 +76,6 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.unsucceeded_course import UnsucceededCourse d = dict(src_dict) - _archived_courses = d.pop("archived_courses", UNSET) - archived_courses: list[CourseCommon] | Unset = UNSET - if _archived_courses is not UNSET: - archived_courses = [] - for archived_courses_item_data in _archived_courses: - archived_courses_item = CourseCommon.from_dict(archived_courses_item_data) - - archived_courses.append(archived_courses_item) - _courses = d.pop("courses", UNSET) courses: list[CourseCommon] | Unset = UNSET if _courses is not UNSET: @@ -93,6 +85,15 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: courses.append(courses_item) + _archived_courses = d.pop("archived_courses", UNSET) + archived_courses: list[CourseCommon] | Unset = UNSET + if _archived_courses is not UNSET: + archived_courses = [] + for archived_courses_item_data in _archived_courses: + archived_courses_item = CourseCommon.from_dict(archived_courses_item_data) + + archived_courses.append(archived_courses_item) + def _parse_in_process_course_actions(data: object) -> list[UnsucceededCourse] | None | Unset: if data is None: return data @@ -118,8 +119,8 @@ def _parse_in_process_course_actions(data: object) -> list[UnsucceededCourse] | in_process_course_actions = _parse_in_process_course_actions(d.pop("in_process_course_actions", UNSET)) course_home_tab = cls( - archived_courses=archived_courses, courses=courses, + archived_courses=archived_courses, in_process_course_actions=in_process_course_actions, ) diff --git a/openedx_platform_sdk/models/paginated_v4_home_courses_response.py b/openedx_platform_sdk/models/paginated_v4_home_courses_response.py new file mode 100644 index 0000000..aa3971f --- /dev/null +++ b/openedx_platform_sdk/models/paginated_v4_home_courses_response.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.course_home_tab_serializer_v2 import CourseHomeTabSerializerV2 + + +T = TypeVar("T", bound="PaginatedV4HomeCoursesResponse") + + +@_attrs_define +class PaginatedV4HomeCoursesResponse: + """ + Attributes: + count (int): Total number of courses. + num_pages (int): Total number of pages. + current_page (int): Current page number. + start (int): Zero-based index of the first item on this page. + next_ (None | str): URL for the next page, or null. + previous (None | str): URL for the previous page, or null. + results (CourseHomeTabSerializerV2): Serializer for course home tab V2 with unsucceeded courses and in process + course actions. + """ + + count: int + num_pages: int + current_page: int + start: int + next_: None | str + previous: None | str + results: CourseHomeTabSerializerV2 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + count = self.count + + num_pages = self.num_pages + + current_page = self.current_page + + start = self.start + + next_: None | str + next_ = self.next_ + + previous: None | str + previous = self.previous + + results = self.results.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "count": count, + "num_pages": num_pages, + "current_page": current_page, + "start": start, + "next": next_, + "previous": previous, + "results": results, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.course_home_tab_serializer_v2 import CourseHomeTabSerializerV2 + + d = dict(src_dict) + count = d.pop("count") + + num_pages = d.pop("num_pages") + + current_page = d.pop("current_page") + + start = d.pop("start") + + def _parse_next_(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + next_ = _parse_next_(d.pop("next")) + + def _parse_previous(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + previous = _parse_previous(d.pop("previous")) + + results = CourseHomeTabSerializerV2.from_dict(d.pop("results")) + + paginated_v4_home_courses_response = cls( + count=count, + num_pages=num_pages, + current_page=current_page, + start=start, + next_=next_, + previous=previous, + results=results, + ) + + paginated_v4_home_courses_response.additional_properties = d + return paginated_v4_home_courses_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py b/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py index e645ade..b1564f6 100644 --- a/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py +++ b/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0.py @@ -11,7 +11,11 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.authoring_grading_grace_period_v0 import AuthoringGradingGracePeriodV0 from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 + from ..models.patchedauthoring_grading_course_grading_v0_grade_cutoffs import ( + PatchedauthoringGradingCourseGradingV0GradeCutoffs, + ) T = TypeVar("T", bound="PatchedauthoringGradingCourseGradingV0") @@ -23,12 +27,22 @@ class PatchedauthoringGradingCourseGradingV0: Attributes: graders (list[AuthoringGradingGradersV0] | None | Unset): + grade_cutoffs (PatchedauthoringGradingCourseGradingV0GradeCutoffs | Unset): Mapping of letter grade to minimum + score (0.0–1.0). Required by CourseGradingModel.update_from_json — must be included in every PATCH. + grace_period (AuthoringGradingGracePeriodV0 | None | Unset): Grace period duration. Pass null to clear the grace + period. + minimum_grade_credit (float | None | Unset): Minimum passing score for credit eligibility (0.0–1.0). """ graders: list[AuthoringGradingGradersV0] | None | Unset = UNSET + grade_cutoffs: PatchedauthoringGradingCourseGradingV0GradeCutoffs | Unset = UNSET + grace_period: AuthoringGradingGracePeriodV0 | None | Unset = UNSET + minimum_grade_credit: float | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.authoring_grading_grace_period_v0 import AuthoringGradingGracePeriodV0 + graders: list[dict[str, Any]] | None | Unset if isinstance(self.graders, Unset): graders = UNSET @@ -41,11 +55,35 @@ def to_dict(self) -> dict[str, Any]: else: graders = self.graders + grade_cutoffs: dict[str, Any] | Unset = UNSET + if not isinstance(self.grade_cutoffs, Unset): + grade_cutoffs = self.grade_cutoffs.to_dict() if not isinstance(self.grade_cutoffs, dict) else dict(self.grade_cutoffs) + + grace_period: dict[str, Any] | None | Unset + if isinstance(self.grace_period, Unset): + grace_period = UNSET + elif isinstance(self.grace_period, AuthoringGradingGracePeriodV0): + grace_period = self.grace_period.to_dict() + else: + grace_period = self.grace_period + + minimum_grade_credit: float | None | Unset + if isinstance(self.minimum_grade_credit, Unset): + minimum_grade_credit = UNSET + else: + minimum_grade_credit = self.minimum_grade_credit + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) if graders is not UNSET: field_dict["graders"] = graders + if grade_cutoffs is not UNSET: + field_dict["grade_cutoffs"] = grade_cutoffs + if grace_period is not UNSET: + field_dict["grace_period"] = grace_period + if minimum_grade_credit is not UNSET: + field_dict["minimum_grade_credit"] = minimum_grade_credit return field_dict @@ -64,6 +102,25 @@ def to_multipart(self) -> types.RequestFiles: else: files.append(("graders", (None, str(self.graders).encode(), "text/plain"))) + if not isinstance(self.grade_cutoffs, Unset): + files.append( + ("grade_cutoffs", (None, json.dumps(self.grade_cutoffs.to_dict()).encode(), "application/json")) + ) + + if not isinstance(self.grace_period, Unset): + if self.grace_period is None: + files.append(("grace_period", (None, str(self.grace_period).encode(), "text/plain"))) + else: + files.append( + ("grace_period", (None, json.dumps(self.grace_period.to_dict()).encode(), "application/json")) + ) + + if not isinstance(self.minimum_grade_credit, Unset): + if isinstance(self.minimum_grade_credit, float): + files.append(("minimum_grade_credit", (None, str(self.minimum_grade_credit).encode(), "text/plain"))) + else: + files.append(("minimum_grade_credit", (None, str(self.minimum_grade_credit).encode(), "text/plain"))) + for prop_name, prop in self.additional_properties.items(): files.append((prop_name, (None, str(prop).encode(), "text/plain"))) @@ -71,7 +128,11 @@ def to_multipart(self) -> types.RequestFiles: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.authoring_grading_grace_period_v0 import AuthoringGradingGracePeriodV0 from ..models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 + from ..models.patchedauthoring_grading_course_grading_v0_grade_cutoffs import ( + PatchedauthoringGradingCourseGradingV0GradeCutoffs, + ) d = dict(src_dict) @@ -97,8 +158,44 @@ def _parse_graders(data: object) -> list[AuthoringGradingGradersV0] | None | Uns graders = _parse_graders(d.pop("graders", UNSET)) + _grade_cutoffs = d.pop("grade_cutoffs", UNSET) + grade_cutoffs: PatchedauthoringGradingCourseGradingV0GradeCutoffs | Unset + if isinstance(_grade_cutoffs, Unset): + grade_cutoffs = UNSET + else: + grade_cutoffs = PatchedauthoringGradingCourseGradingV0GradeCutoffs.from_dict(_grade_cutoffs) + + def _parse_grace_period(data: object) -> AuthoringGradingGracePeriodV0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + grace_period_type_1 = AuthoringGradingGracePeriodV0.from_dict(data) + + return grace_period_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AuthoringGradingGracePeriodV0 | None | Unset, data) + + grace_period = _parse_grace_period(d.pop("grace_period", UNSET)) + + def _parse_minimum_grade_credit(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + minimum_grade_credit = _parse_minimum_grade_credit(d.pop("minimum_grade_credit", UNSET)) + patchedauthoring_grading_course_grading_v0 = cls( graders=graders, + grade_cutoffs=grade_cutoffs, + grace_period=grace_period, + minimum_grade_credit=minimum_grade_credit, ) patchedauthoring_grading_course_grading_v0.additional_properties = d diff --git a/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0_grade_cutoffs.py b/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0_grade_cutoffs.py new file mode 100644 index 0000000..3696e47 --- /dev/null +++ b/openedx_platform_sdk/models/patchedauthoring_grading_course_grading_v0_grade_cutoffs.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PatchedauthoringGradingCourseGradingV0GradeCutoffs") + + +@_attrs_define +class PatchedauthoringGradingCourseGradingV0GradeCutoffs: + """Mapping of letter grade to minimum score (0.0–1.0). Required by CourseGradingModel.update_from_json — must be + included in every PATCH. + + """ + + additional_properties: dict[str, float] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + patchedauthoring_grading_course_grading_v0_grade_cutoffs = cls() + + patchedauthoring_grading_course_grading_v0_grade_cutoffs.additional_properties = d + return patchedauthoring_grading_course_grading_v0_grade_cutoffs + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> float: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: float) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/v1_xblock_retrieve_view.py b/openedx_platform_sdk/models/v1_xblock_retrieve_view.py new file mode 100644 index 0000000..b17ab24 --- /dev/null +++ b/openedx_platform_sdk/models/v1_xblock_retrieve_view.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class V1XblockRetrieveView(str, Enum): + MINIMAL = "minimal" + + def __str__(self) -> str: + return str(self.value) diff --git a/regen_sdk.sh b/regen_sdk.sh index c73d6fc..505409c 100755 --- a/regen_sdk.sh +++ b/regen_sdk.sh @@ -83,6 +83,101 @@ if [[ -f /tmp/_sdk_auth.py ]]; then "$SCRIPT_DIR/openedx_platform_sdk/__init__.py" fi +# Fix generator bugs in API modules that have nested-object bodies. +API_DIR="$SCRIPT_DIR/openedx_platform_sdk/api/openedx_platform_sdk" + +# Bug 1: v3_course_details_update uses Unset in type annotations but imports only UNSET. +UPDATE_FILE="$API_DIR/v3_course_details_update.py" +if [[ -f "$UPDATE_FILE" ]]; then + sed -i '' 's/from \.\.\.types import UNSET, Response$/from ...types import UNSET, Response, Unset/' "$UPDATE_FILE" +fi + +# Bug 2: _get_kwargs has three identical isinstance(body, X) blocks for json/form/multipart. +# The multipart block always wins and breaks nested-dict payloads. Keep only JSON block. +python3 - "$API_DIR" <<'PYEOF' +import sys, os, glob + +api_dir = sys.argv[1] + +for path in glob.glob(os.path.join(api_dir, "*.py")): + lines = open(path).readlines() + # Find the _get_kwargs function boundaries + in_get_kwargs = False + new_lines = [] + i = 0 + changed = False + while i < len(lines): + line = lines[i] + if "def _get_kwargs(" in line: + in_get_kwargs = True + if in_get_kwargs and line.strip() == "return _kwargs": + in_get_kwargs = False + # Inside _get_kwargs, when we see the second "if isinstance(body" block, + # skip until we see " _kwargs["headers"] = headers" (un-indented in func) + if in_get_kwargs and line.startswith(" if isinstance(body,"): + # Check if the next non-blank, non-comment line sets _kwargs["json"] + # by looking at what comes after this block + block_start = i + # Look ahead to see if the following line (possibly with inner if) sets json + j = i + 1 + while j < len(lines) and (lines[j].strip() == "" or lines[j].startswith(" ")): + j += 1 + # j now points past this if-block. If j points to another "if isinstance(body", + # this is a duplicate block — skip it if it contains "data" or "files" + block_content = "".join(lines[i:j]) + if '_kwargs["json"]' not in block_content and ( + '_kwargs["data"]' in block_content or '_kwargs["files"]' in block_content + ): + changed = True + i = j + continue + new_lines.append(line) + i += 1 + if changed: + open(path, 'w').writelines(new_lines) + print(f" Fixed multipart bug in {os.path.basename(path)}") +PYEOF + +# Bug 3: DictField wrapper models — to_dict() calls .to_dict() on the field unconditionally, +# but users pass plain Python dicts. Fix: make the call dict-safe. +# Pattern: `if not isinstance(self.X, Unset):` followed by `X = self.X.to_dict()` +MODELS_DIR="$SCRIPT_DIR/openedx_platform_sdk/models" +python3 - "$MODELS_DIR" <<'PYEOF' +import sys, os, glob, re + +models_dir = sys.argv[1] +for path in glob.glob(os.path.join(models_dir, "*.py")): + lines = open(path).readlines() + new_lines = [] + changed = False + i = 0 + while i < len(lines): + line = lines[i] + m = re.match(r'^(\s+)if not isinstance\(self\.(\w+), Unset\):\s*$', line) + if m and i + 1 < len(lines): + field = m.group(2) + next_line = lines[i + 1] + nm = re.match( + r'^(\s+)' + re.escape(field) + r' = self\.' + re.escape(field) + r'\.to_dict\(\)\s*$', + next_line, + ) + if nm: + inner_indent = nm.group(1) + new_lines.append(line) + new_lines.append( + f"{inner_indent}{field} = self.{field}.to_dict() " + f"if not isinstance(self.{field}, dict) else dict(self.{field})\n" + ) + changed = True + i += 2 + continue + new_lines.append(line) + i += 1 + if changed: + open(path, 'w').writelines(new_lines) + print(f" Fixed dict-safe to_dict() in {os.path.basename(path)}") +PYEOF + echo "" echo "✓ SDK regenerated successfully." echo " Package : openedx_platform_sdk" From a3788f57baa0fb2bbc3fa59dabd76067baf53a23 Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Fri, 3 Jul 2026 16:06:38 +0500 Subject: [PATCH 7/8] docs: add testing-sdk-apis.rst with typed usage examples for all API groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers all five API groups exposed by the SDK with working, copy-paste ready code snippets: - Home v3: retrieve (studio name + courses + libraries), courses-only, libraries-only endpoints - Home v4: paginated courses with count / num_pages / results traversal - Course Details v3: retrieve and PUT round-trip (retrieve → mutate → update → restore pattern) - Authoring Grading v3: PATCH with typed grade_cutoffs, grace_period, and minimum_grade_credit fields (no more additional_properties hack) - XBlock v1: retrieve, create, partial_update (rename), destroy — including a single end-to-end lifecycle example Also updates README to: - Use typed SDK calls (v3_home_retrieve, v4_home_courses_retrieve) in the Authentication, Usage, and Testing Locally sections instead of the raw client.get_httpx_client().request() call - Add a link to docs/testing-sdk-apis.rst from the Testing Locally section for readers who want the full per-API examples Co-Authored-By: Claude Sonnet 4.6 --- docs/testing-sdk-apis.rst | 349 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 docs/testing-sdk-apis.rst diff --git a/docs/testing-sdk-apis.rst b/docs/testing-sdk-apis.rst new file mode 100644 index 0000000..21d7f63 --- /dev/null +++ b/docs/testing-sdk-apis.rst @@ -0,0 +1,349 @@ +Testing SDK APIs Locally +======================== + +This guide shows typed SDK usage for every API group exposed by ``openedx-platform-sdk``. +All examples assume you have a running devstack or Tutor instance and an OAuth2 application +configured as described in the `Testing Locally <../README.md#testing-locally>`_ section of +the README. + +.. contents:: APIs covered + :local: + :depth: 1 + +---- + +Setup +----- + +Install the SDK in editable mode and configure credentials: + +.. code-block:: python + + from openedx_platform_sdk import OAuth2ClientCredentials + + auth = OAuth2ClientCredentials( + lms_url="http://local.openedx.io:8000", + client_id="your-client-id", + client_secret="your-client-secret", + ) + + STUDIO_URL = "http://studio.local.openedx.io:8001/api/contentstore" + + # All examples below run inside this context manager + with auth.get_client(studio_url=STUDIO_URL) as client: + ... + +.. note:: + + ``studio_url`` **must** include ``/api/contentstore``. + The SDK appends versioned paths (e.g. ``/v3/home/``) directly to this base. + +---- + +Home v3 +------- + +Retrieve the Studio home page (studio name, course list, library list): + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_retrieve + + with auth.get_client(studio_url=STUDIO_URL) as client: + home = v3_home_retrieve.sync(client=client) + + print(home.studio_name) # e.g. "Your Studio" + print(home.courses) # list of course summaries + print(home.libraries) # list of library summaries + +Retrieve only courses (lighter response): + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_courses_retrieve + + with auth.get_client(studio_url=STUDIO_URL) as client: + result = v3_home_courses_retrieve.sync(client=client) + + for course in result.courses: + print(course.course_key, course.display_name) + +Retrieve only libraries: + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v3_home_libraries_retrieve + + with auth.get_client(studio_url=STUDIO_URL) as client: + result = v3_home_libraries_retrieve.sync(client=client) + + for lib in result.libraries: + print(lib.library_key, lib.display_name) + +---- + +Home v4 (paginated courses) +---------------------------- + +The v4 endpoint returns a paginated wrapper with ``count``, ``num_pages``, ``current_page``, +``start``, ``next_``, ``previous``, and a ``results`` object that holds the course list: + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v4_home_courses_retrieve + + with auth.get_client(studio_url=STUDIO_URL) as client: + page = v4_home_courses_retrieve.sync(client=client) + + print(f"Total courses : {page.count}") + print(f"Total pages : {page.num_pages}") + print(f"Current page : {page.current_page}") + + for course in page.results.courses: + print(course.course_key, course.display_name) + +.. note:: + + ``page.results`` is a single ``CourseHomeTabSerializerV2`` object — it is *not* a list. + Iterate ``page.results.courses`` (a list) to reach individual courses. + +---- + +Course Details v3 +----------------- + +Retrieve course details +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v3_course_details_retrieve + + COURSE_KEY = "course-v1:org+course+run" + + with auth.get_client(studio_url=STUDIO_URL) as client: + details = v3_course_details_retrieve.sync(client=client, course_id=COURSE_KEY) + + print(details.course_id) + print(details.org) + print(details.self_paced) + print(details.start_date) + print(details.end_date) + +Update course details (PUT round-trip) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The update endpoint expects a full ``CourseDetails`` object. The simplest pattern is to +retrieve first, mutate what you need, then PUT it back: + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import ( + v3_course_details_retrieve, + v3_course_details_update, + ) + + COURSE_KEY = "course-v1:org+course+run" + + with auth.get_client(studio_url=STUDIO_URL) as client: + details = v3_course_details_retrieve.sync(client=client, course_id=COURSE_KEY) + + # Toggle self-paced flag + details.self_paced = not details.self_paced + + updated = v3_course_details_update.sync( + client=client, + course_id=COURSE_KEY, + body=details, + ) + print(updated.self_paced) # reflects the new value + +---- + +Authoring Grading v3 +--------------------- + +The grading PATCH endpoint requires ``grade_cutoffs`` and ``grace_period`` in the request body +even though they are not part of the generated ``PatchedauthoringGradingCourseGradingV0`` schema. +Pass them via ``additional_properties``: + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v3_authoring_grading_partial_update + from openedx_platform_sdk.models.authoring_grading_graders_v0 import AuthoringGradingGradersV0 + from openedx_platform_sdk.models.patchedauthoring_grading_course_grading_v0 import ( + PatchedauthoringGradingCourseGradingV0, + ) + + COURSE_KEY = "course-v1:org+course+run" + + with auth.get_client(studio_url=STUDIO_URL) as client: + body = PatchedauthoringGradingCourseGradingV0( + graders=[ + AuthoringGradingGradersV0( + type_="Homework", + min_count=1, + drop_count=0, + weight=100, + id=0, + short_label="HW", + ) + ], + grade_cutoffs={"Pass": 0.5}, + grace_period={"hours": 0, "minutes": 0}, + minimum_grade_credit=0.7, + ) + + result = v3_authoring_grading_partial_update.sync( + client=client, + course_key=COURSE_KEY, + body=body, + ) + print(result.graders) # list of grader dicts + print(result.grade_cutoffs) # {"Pass": 0.5} + +.. note:: + + ``grade_cutoffs`` and ``grace_period`` are required by ``CourseGradingModel.update_from_json`` + on the platform — always include them in every PATCH even though the endpoint is partial-update. + +---- + +XBlock v1 +---------- + +Retrieve an XBlock +~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v1_xblock_retrieve + + COURSE_USAGE_KEY = "block-v1:org+course+run+type@course+block@course" + + with auth.get_client(studio_url=STUDIO_URL) as client: + xblock = v1_xblock_retrieve.sync( + client=client, + usage_key_string=COURSE_USAGE_KEY, + ) + print(xblock.id) # usage key + print(xblock.category) # "course" + print(xblock.display_name) + +Create an XBlock +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v1_xblock_create + from openedx_platform_sdk.models.xblock import Xblock + + COURSE_USAGE_KEY = "block-v1:org+course+run+type@course+block@course" + + with auth.get_client(studio_url=STUDIO_URL) as client: + body = Xblock( + parent_locator=COURSE_USAGE_KEY, + category="chapter", + display_name="My New Section", + ) + # Use sync_detailed to inspect the status code + resp = v1_xblock_create.sync_detailed(client=client, body=body) + + print(resp.status_code.value) # 200 + locator = resp.parsed.additional_properties.get("locator") + print(locator) # usage key of the new block + +Update (partial) an XBlock +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v1_xblock_partial_update + from openedx_platform_sdk.models.patched_xblock import PatchedXblock + + with auth.get_client(studio_url=STUDIO_URL) as client: + body = PatchedXblock(display_name="Renamed Section") + resp = v1_xblock_partial_update.sync_detailed( + client=client, + usage_key_string=locator, # locator from the create step above + body=body, + ) + print(resp.status_code.value) # 200 + +Delete an XBlock +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v1_xblock_destroy + + with auth.get_client(studio_url=STUDIO_URL) as client: + resp = v1_xblock_destroy.sync_detailed( + client=client, + usage_key_string=locator, + ) + print(resp.status_code.value) # 204 + +Full XBlock lifecycle (create → rename → delete) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import ( + v1_xblock_create, + v1_xblock_destroy, + v1_xblock_partial_update, + ) + from openedx_platform_sdk.models.patched_xblock import PatchedXblock + from openedx_platform_sdk.models.xblock import Xblock + + COURSE_USAGE_KEY = "block-v1:org+course+run+type@course+block@course" + + with auth.get_client(studio_url=STUDIO_URL) as client: + # 1. Create + resp = v1_xblock_create.sync_detailed( + client=client, + body=Xblock( + parent_locator=COURSE_USAGE_KEY, + category="chapter", + display_name="Temp Section", + ), + ) + locator = resp.parsed.additional_properties["locator"] + + # 2. Rename + v1_xblock_partial_update.sync_detailed( + client=client, + usage_key_string=locator, + body=PatchedXblock(display_name="Temp Section (Renamed)"), + ) + + # 3. Delete + v1_xblock_destroy.sync_detailed( + client=client, + usage_key_string=locator, + ) + +---- + +Running the Integration Test Suite +------------------------------------ + +A ready-made integration test script covering all of the above is located at the repo root: + +.. code-block:: bash + + cd /path/to/nightly + pip install -e openedx-platform-sdk/ + python test_sdk.py + +Expected output (all 27 checks green):: + + ─── Home v3 ─── + [PASS] v3_home_retrieve returns a result + [PASS] v3_home_retrieve has studio_name + [PASS] v3_home_retrieve has courses list + [PASS] v3_home_courses_retrieve returns a result + ... + Results: 27/27 tests passed + All tests passed! From e93668889ed183e766db43270317566a0de64bf7 Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Mon, 6 Jul 2026 12:46:47 +0500 Subject: [PATCH 8/8] feat: add LMS Enrollment v2 API bindings and schema merge pipeline Extends the SDK to cover the LMS Enrollment v2 API by merging an LMS drf-spectacular schema into the Studio schema during regen_sdk.sh. SDK changes: - regen_sdk.sh: download LMS schema from /lms-api/schema/, pass --merge to filter_schema.py; add Bug 5 fix (plain-list response for enrollment_allowed) - filter_schema.py: add merge_schema(), fix_path_parameters() to strip spurious path params that cause the generator to skip endpoints; add --merge CLI flag - New API modules: v2_enrollment_*, v2_course_retrieve, v2_roles_retrieve, v2_enrollments_list (11 files) - New models: CourseEnrollment, EnrollmentCourse, CourseEnrollmentAllowed, and supporting paginated/response models (13 files) - lms_schema.yml: cached LMS enrollment schema Bug fixes applied at regen time: - Bug 4: null-safe datetime parsing in EnrollmentCourse.from_dict() - Bug 5: plain-list normalisation in PaginatedCourseEnrollmentAllowedList Docs: - README.md: add Enrollment v2 to covered APIs table, update regen steps - docs/testing-sdk-apis.rst: update enrollment section with correct module names, base URL (/api/enrollment), and accurate LMS schema note Co-Authored-By: Claude Sonnet 4.6 --- README.md | 28 +- docs/testing-sdk-apis.rst | 113 +- filter_schema.py | 61 +- lms_schema.yml | 1032 +++++++++++++++++ .../v2_course_retrieve.py | 194 ++++ .../v2_enrollment_create.py | 229 ++++ ...v2_enrollment_enrollment_allowed_create.py | 261 +++++ ...2_enrollment_enrollment_allowed_destroy.py | 198 ++++ ..._enrollment_enrollment_allowed_retrieve.py | 198 ++++ .../v2_enrollment_list.py | 234 ++++ .../v2_enrollment_retrieve.py | 187 +++ .../v2_enrollment_unenroll_create.py | 134 +++ .../v2_enrollments_list.py | 304 +++++ .../openedx_platform_sdk/v2_roles_retrieve.py | 191 +++ openedx_platform_sdk/auth.py | 5 +- openedx_platform_sdk/models/__init__.py | 26 + .../authoring_grading_course_grading_v0.py | 2 +- openedx_platform_sdk/models/course_details.py | 3 +- .../models/course_enrollment.py | 151 +++ .../models/course_enrollment_allowed.py | 158 +++ .../models/course_enrollments_api_list.py | 118 ++ .../models/enrollment_course.py | 131 +++ ...aginated_course_enrollment_allowed_list.py | 127 ++ .../paginated_course_enrollment_list.py | 125 ++ ...inated_course_enrollments_api_list_list.py | 117 ++ openedx_platform_sdk/models/user_role.py | 78 ++ .../models/user_roles_response.py | 84 ++ .../models/v2_enrollment_list_view.py | 8 + ...v2_enrollment_unenroll_create_data_body.py | 61 + ...2_enrollment_unenroll_create_files_body.py | 73 ++ ...v2_enrollment_unenroll_create_json_body.py | 61 + regen_sdk.sh | 118 +- 32 files changed, 4750 insertions(+), 60 deletions(-) create mode 100644 lms_schema.yml create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_course_retrieve.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_create.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_create.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_destroy.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_retrieve.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_list.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_retrieve.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_unenroll_create.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollments_list.py create mode 100644 openedx_platform_sdk/api/openedx_platform_sdk/v2_roles_retrieve.py create mode 100644 openedx_platform_sdk/models/course_enrollment.py create mode 100644 openedx_platform_sdk/models/course_enrollment_allowed.py create mode 100644 openedx_platform_sdk/models/course_enrollments_api_list.py create mode 100644 openedx_platform_sdk/models/enrollment_course.py create mode 100644 openedx_platform_sdk/models/paginated_course_enrollment_allowed_list.py create mode 100644 openedx_platform_sdk/models/paginated_course_enrollment_list.py create mode 100644 openedx_platform_sdk/models/paginated_course_enrollments_api_list_list.py create mode 100644 openedx_platform_sdk/models/user_role.py create mode 100644 openedx_platform_sdk/models/user_roles_response.py create mode 100644 openedx_platform_sdk/models/v2_enrollment_list_view.py create mode 100644 openedx_platform_sdk/models/v2_enrollment_unenroll_create_data_body.py create mode 100644 openedx_platform_sdk/models/v2_enrollment_unenroll_create_files_body.py create mode 100644 openedx_platform_sdk/models/v2_enrollment_unenroll_create_json_body.py diff --git a/README.md b/README.md index f20c8b8..d918927 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Python client library for the [OpenedX Authoring API](https://docs.openedx.org), auto-generated from the platform's OpenAPI schema using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client). -Covers the standardized v1/v3/v4 APIs tagged `openedx-platform-sdk` in the platform. +Covers the standardized v1/v3/v4 Studio APIs and LMS Enrollment v2 APIs tagged `openedx-platform-sdk` in the platform. --- @@ -15,6 +15,7 @@ Covers the standardized v1/v3/v4 APIs tagged `openedx-platform-sdk` in the platf | Course Details | v3 | retrieve, update | | Home | v3 | list, courses, libraries | | Home (paginated) | v4 | courses | +| Enrollment | v2 | list, retrieve, create, enrollment_allowed, enrollments, roles, course | --- @@ -30,31 +31,18 @@ pip install openapi-python-client pyyaml ### Steps -```bash -# 1. Download the schema from a running Studio instance -curl http://studio.local.openedx.io:8001/authoring-api/schema/ > schema.yml - -# 2. Filter schema to only openedx-platform-sdk tagged paths -python filter_schema.py schema.yml filtered_schema.yml openedx-platform-sdk - -# 3. Regenerate the SDK -openapi-python-client generate \ - --path filtered_schema.yml \ - --config config.yml -``` - -Or use the provided script: +Use the provided script — it downloads schemas from both Studio and LMS, merges them, and regenerates the SDK: ```bash -# Regenerate from a running Studio (no platform repo needed) +# Regenerate from running Studio + LMS instances ./regen_sdk.sh # Checkout a specific branch first, then regenerate # PLATFORM_DIR defaults to ../openedx-platform — override if your checkout is elsewhere PLATFORM_DIR=/path/to/openedx-platform ./regen_sdk.sh feat/axim-api_improvements -# Use a different Studio URL -STUDIO_URL=http://studio.example.com:8001 ./regen_sdk.sh +# Use different URLs +STUDIO_URL=http://studio.example.com:8001 LMS_URL=http://lms.example.com:8000 ./regen_sdk.sh ``` --- @@ -188,8 +176,10 @@ with auth.get_client(studio_url="http://studio.local.openedx.io:8001/api/content ``` > **Note:** The `studio_url` must include `/api/contentstore` — the SDK appends versioned paths (e.g. `/v3/home/`) directly to this base. +> +> For Enrollment v2 APIs, use `http://local.openedx.io:8000/api/enrollment` as the base URL instead — enrollment endpoints live on LMS, not Studio. -For typed usage examples covering all API groups (Home v3/v4, Course Details, Authoring Grading, XBlock lifecycle), see **[docs/testing-sdk-apis.rst](docs/testing-sdk-apis.rst)**. +For typed usage examples covering all API groups (Home v3/v4, Course Details, Authoring Grading, XBlock lifecycle, Enrollment v2), see **[docs/testing-sdk-apis.rst](docs/testing-sdk-apis.rst)**. --- diff --git a/docs/testing-sdk-apis.rst b/docs/testing-sdk-apis.rst index 21d7f63..1167aa9 100644 --- a/docs/testing-sdk-apis.rst +++ b/docs/testing-sdk-apis.rst @@ -326,24 +326,105 @@ Full XBlock lifecycle (create → rename → delete) ---- -Running the Integration Test Suite ------------------------------------- +Enrollment v2 +------------- -A ready-made integration test script covering all of the above is located at the repo root: +.. note:: + + Enrollment APIs live in LMS (``openedx.core.djangoapps.enrollments``). + The SDK schema is generated from both Studio and LMS and merged at build time. + + **At runtime, the enrollment client must target the LMS enrollment base URL, not Studio.** + +.. code-block:: python -.. code-block:: bash + LMS_ENROLLMENT_URL = "http://local.openedx.io:8000/api/enrollment" + + # Use a separate client pointed at LMS + with auth.get_client(studio_url=LMS_ENROLLMENT_URL) as enroll_client: + ... + +List enrollments for the authenticated user +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python - cd /path/to/nightly - pip install -e openedx-platform-sdk/ - python test_sdk.py + from openedx_platform_sdk.api.openedx_platform_sdk import v2_enrollment_list -Expected output (all 27 checks green):: + with auth.get_client(studio_url=LMS_ENROLLMENT_URL) as enroll_client: + result = v2_enrollment_list.sync(client=enroll_client) + print(result.count) + for enrollment in result.results: + print(enrollment.is_active, enrollment.mode) - ─── Home v3 ─── - [PASS] v3_home_retrieve returns a result - [PASS] v3_home_retrieve has studio_name - [PASS] v3_home_retrieve has courses list - [PASS] v3_home_courses_retrieve returns a result - ... - Results: 27/27 tests passed - All tests passed! +Get course enrollment details +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v2_course_retrieve + + COURSE_KEY = "course-v1:org+course+run" + + with auth.get_client(studio_url=LMS_ENROLLMENT_URL) as enroll_client: + course = v2_course_retrieve.sync(course_id=COURSE_KEY, client=enroll_client) + print(course.course_id) + print(course.invite_only) + +Get user roles +~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v2_roles_retrieve + + with auth.get_client(studio_url=LMS_ENROLLMENT_URL) as enroll_client: + roles = v2_roles_retrieve.sync(client=enroll_client) + print(roles.roles) + +Admin enrollment list +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from openedx_platform_sdk.api.openedx_platform_sdk import v2_enrollments_list + + with auth.get_client(studio_url=LMS_ENROLLMENT_URL) as enroll_client: + result = v2_enrollments_list.sync(client=enroll_client, course_id=COURSE_KEY) + for item in result.results: + print(item.user, item.mode, item.is_active) + +Enroll and retrieve +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + import datetime + from openedx_platform_sdk.api.openedx_platform_sdk import ( + v2_enrollment_create, + v2_enrollment_retrieve, + ) + from openedx_platform_sdk.models.course_enrollment import CourseEnrollment + from openedx_platform_sdk.models.enrollment_course import EnrollmentCourse + + epoch = datetime.datetime(2024, 1, 1) + body = CourseEnrollment( + created=None, + user="", # empty → LMS uses the OAuth2 service account's own user + mode="audit", + course_details=EnrollmentCourse( + course_id=COURSE_KEY, course_name="", invite_only=False, + course_modes="", pacing_type="", + enrollment_start=epoch, enrollment_end=epoch, + course_start=epoch, course_end=epoch, + ), + ) + + with auth.get_client(studio_url=LMS_ENROLLMENT_URL) as enroll_client: + resp = v2_enrollment_create.sync_detailed(client=enroll_client, body=body) + print(resp.status_code.value) # 200 + + enrollment = v2_enrollment_retrieve.sync( + client=enroll_client, username="your-lms-username", course_id=COURSE_KEY + ) + print(enrollment.is_active) # True diff --git a/filter_schema.py b/filter_schema.py index 07b3c4f..7a3d509 100644 --- a/filter_schema.py +++ b/filter_schema.py @@ -2,12 +2,15 @@ """ Filter an OpenAPI schema to only include paths tagged with a specific tag. Recursively resolves all $ref dependencies so the filtered schema is self-contained. +Optionally merges a second schema (e.g. LMS enrollment schema) before filtering. Usage: python filter_schema.py schema.yml filtered_schema.yml openedx-platform-sdk + python filter_schema.py schema.yml filtered_schema.yml openedx-platform-sdk --merge lms_schema.yml """ import sys + import yaml @@ -16,7 +19,6 @@ def collect_refs(obj, refs: set) -> None: if isinstance(obj, dict): if "$ref" in obj: ref = obj["$ref"] - # e.g. '#/components/schemas/Xblock' -> 'Xblock' if ref.startswith("#/components/schemas/"): refs.add(ref.split("/")[-1]) for v in obj.values(): @@ -45,10 +47,49 @@ def resolve_all_refs(names: set, components: dict) -> set: return resolved -def filter_schema(input_path: str, output_path: str, tag: str) -> None: +def merge_schema(base: dict, extra: dict) -> dict: + """Merge paths and components from extra into base (base takes precedence on conflict).""" + for path, path_item in extra.get("paths", {}).items(): + if path not in base.setdefault("paths", {}): + base["paths"][path] = path_item + + extra_schemas = extra.get("components", {}).get("schemas", {}) + base_schemas = base.setdefault("components", {}).setdefault("schemas", {}) + for name, schema_def in extra_schemas.items(): + if name not in base_schemas: + base_schemas[name] = schema_def + + return base + + +def fix_path_parameters(paths: dict) -> None: + """Remove path parameters that are declared but not present in the URL template.""" + import re + for path, path_item in paths.items(): + template_params = set(re.findall(r"\{(\w+)\}", path)) + for method, operation in path_item.items(): + if not isinstance(operation, dict): + continue + params = operation.get("parameters", []) + fixed = [ + p for p in params + if not (p.get("in") == "path" and p.get("name") not in template_params) + ] + if len(fixed) != len(params): + operation["parameters"] = fixed + + +def filter_schema(input_path: str, output_path: str, tag: str, merge_path: str | None = None) -> None: with open(input_path) as f: schema = yaml.safe_load(f) + # Optionally merge a second schema before filtering + if merge_path: + with open(merge_path) as f: + extra_schema = yaml.safe_load(f) + schema = merge_schema(schema, extra_schema) + print(f"Merged additional schema from: {merge_path}") + # 1. Filter paths to only tagged operations filtered_paths = {} for path, path_item in schema.get("paths", {}).items(): @@ -63,6 +104,10 @@ def filter_schema(input_path: str, output_path: str, tag: str) -> None: schema["paths"] = filtered_paths + # Fix: remove path parameters declared but absent from the URL template + # (e.g. /v2/enrollment/{course_id} incorrectly declares "username" as a path param) + fix_path_parameters(filtered_paths) + # 2. Collect all $refs used directly in filtered paths direct_refs: set = set() collect_refs(filtered_paths, direct_refs) @@ -89,7 +134,13 @@ def filter_schema(input_path: str, output_path: str, tag: str) -> None: if __name__ == "__main__": - if len(sys.argv) != 4: - print(f"Usage: {sys.argv[0]} ") + if len(sys.argv) < 4: + print(f"Usage: {sys.argv[0]} [--merge ]") sys.exit(1) - filter_schema(sys.argv[1], sys.argv[2], sys.argv[3]) + + merge_path = None + if "--merge" in sys.argv: + idx = sys.argv.index("--merge") + merge_path = sys.argv[idx + 1] + + filter_schema(sys.argv[1], sys.argv[2], sys.argv[3], merge_path=merge_path) diff --git a/lms_schema.yml b/lms_schema.yml new file mode 100644 index 0000000..459b94d --- /dev/null +++ b/lms_schema.yml @@ -0,0 +1,1032 @@ +openapi: 3.0.3 +info: + title: LMS Enrollment API + version: 0.1.0 +paths: + /v1/course/{course_id}: + get: + operationId: v1_course_retrieve + description: |- + Read enrollment information for a particular course. + + HTTP Endpoint for retrieving course level enrollment information. + + Args: + request (Request): To get current course enrollment information, a GET request will return + information for the specified course. + course_id (str): URI element specifying the course location. Enrollment information will be + returned. + + Return: + A JSON serialized representation of the course enrollment details. + parameters: + - in: path + name: course_id + schema: + type: string + pattern: ^[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+$ + required: true + tags: + - v1 + responses: + '200': + description: No response body + /v1/enrollment: + get: + operationId: v1_enrollment_retrieve + description: |- + Gets a list of all course enrollments for a user. + + Returns a list for the currently logged in user, or for the user named by the 'user' GET + parameter. If the username does not match that of the currently logged in user, only + courses for which the currently logged in user has the Staff or Admin role are listed. + As a result, a course team member can find out which of their own courses a particular + learner is enrolled in. + + Only the Staff or Admin role (granted on the Django administrative console as the staff + or instructor permission) in individual courses gives the requesting user access to + enrollment data. Permissions granted at the organizational level do not give a user + access to enrollment data for all of that organization's courses. + + Users who have the global staff permission can access all enrollment data for all + courses. + tags: + - v1 + responses: + '200': + description: No response body + post: + operationId: v1_enrollment_create + description: |- + Enrolls the currently logged-in user in a course. + + Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests + go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments. + tags: + - v1 + responses: + '200': + description: No response body + /v1/enrollment/{course_id}: + get: + operationId: v1_enrollment_retrieve_2 + description: |- + Create, read, or update enrollment information for a user. + + HTTP Endpoint for all CRUD operations for a user course enrollment. Allows creation, reading, and + updates of the current enrollment for a particular course. + + Args: + request (Request): To get current course enrollment information, a GET request will return + information for the current user and the specified course. + course_id (str): URI element specifying the course location. Enrollment information will be + returned, created, or updated for this particular course. + username (str): The username associated with this enrollment request. + + Return: + A JSON serialized representation of the course enrollment. + parameters: + - in: path + name: course_id + schema: + type: string + pattern: ^[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+$ + required: true + tags: + - v1 + responses: + '200': + description: No response body + /v1/enrollment/{username},{course_id}: + get: + operationId: v1_enrollment_,_retrieve + description: |- + Create, read, or update enrollment information for a user. + + HTTP Endpoint for all CRUD operations for a user course enrollment. Allows creation, reading, and + updates of the current enrollment for a particular course. + + Args: + request (Request): To get current course enrollment information, a GET request will return + information for the current user and the specified course. + course_id (str): URI element specifying the course location. Enrollment information will be + returned, created, or updated for this particular course. + username (str): The username associated with this enrollment request. + + Return: + A JSON serialized representation of the course enrollment. + parameters: + - in: path + name: course_id + schema: + type: string + pattern: ^[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+$ + required: true + - in: path + name: username + schema: + type: string + pattern: ^[\w .@_+-]+$ + required: true + tags: + - v1 + responses: + '200': + description: No response body + /v1/enrollment_allowed/: + get: + operationId: v1_enrollment_allowed_retrieve + description: |- + Returns the enrollments allowed for a given user email. + + **Example Requests** + + GET /api/enrollment/v1/enrollment_allowed?email=user@example.com + + **Parameters** + + - `email` (optional, string, _query_params_) - defaults to the calling user if not provided. + + **Responses** + - 200: Success. + - 403: Forbidden, you need to be staff. + tags: + - v1 + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + description: '' + post: + operationId: v1_enrollment_allowed_create + description: |- + Creates an enrollment allowed for a given user email and course id. + + **Example Request** + + POST /api/enrollment/v1/enrollment_allowed/ + + Note: The URL for this request must finish with / + + Example request data: + ``` + { + "email": "user@example.com", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "auto_enroll": true + } + ``` + + **Parameters** + + - `email` (**required**, string, _body_) + + - `course_id` (**required**, string, _body_) + + - `auto_enroll` (optional, bool: default=false, _body_) + + **Responses** + - 400: Bad request, missing data. + - 403: Forbidden, you need to be staff. + - 409: Conflict, enrollment allowed already exists. + tags: + - v1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + multipart/form-data: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + description: '' + delete: + operationId: v1_enrollment_allowed_destroy + description: |- + Deletes an enrollment allowed for a given user email and course id. + + **Example Request** + + DELETE /api/enrollment/v1/enrollment_allowed/ + + Note: The URL for this request must finish with / + + Example request data: + ``` + { + "email": "user@example.com", + "course_id": "course-v1:edX+DemoX+Demo_Course" + } + ``` + + **Parameters** + + - `email` (**required**, string, _body_) + + - `course_id` (**required**, string, _body_) + + **Responses** + - 204: Enrollment allowed deleted. + - 400: Bad request, missing data. + - 403: Forbidden, you need to be staff. + - 404: Not found, the course enrollment allowed doesn't exists. + tags: + - v1 + responses: + '204': + description: No response body + /v1/enrollments/: + get: + operationId: v1_enrollments_list + description: |- + **Use Cases** + + Get a list of all course enrollments, optionally filtered by a course ID or list of usernames. + + **Example Requests** + + GET /api/enrollment/v1/enrollments + + GET /api/enrollment/v1/enrollments?course_id={course_id} + + GET /api/enrollment/v1/enrollments?course_ids={course_id},{course_id},{course_id} + + GET /api/enrollment/v1/enrollments?username={username},{username},{username} + + GET /api/enrollment/v1/enrollments?course_id={course_id}&username={username} + + GET /api/enrollment/v1/enrollments?email={email},{email} + + **Query Parameters for GET** + + * course_id: Filters the result to course enrollments for the course corresponding to the + given course ID. The value must be URL encoded. Optional. + + * course_ids: List of comma-separated course IDs. Filters the result to course enrollments + for the courses corresponding to the given course IDs. Course IDs could be course run IDs + or course IDs. The value must be URL encoded. Optional. + + * username: List of comma-separated usernames. Filters the result to the course enrollments + of the given users. Optional. + + * email: List of comma-separated emails. Filters the result to the course enrollments + of the given users. Optional. + + * page_size: Number of results to return per page. Optional. + + * page: Page number to retrieve. Optional. + + **Response Values** + + If the request for information about the course enrollments is successful, an HTTP 200 "OK" response + is returned. + + The HTTP 200 response has the following values. + + * results: A list of the course enrollments matching the request. + + * created: Date and time when the course enrollment was created. + + * mode: Mode for the course enrollment. + + * is_active: Whether the course enrollment is active or not. + + * user: Username of the user in the course enrollment. + + * course_id: Course ID of the course in the course enrollment. + + * next: The URL to the next page of results, or null if this is the + last page. + + * previous: The URL to the next page of results, or null if this + is the first page. + + If the user is not logged in, a 401 error is returned. + + If the user is not global staff, a 403 error is returned. + + If the specified course_id is not valid or any of the specified usernames + are not valid, a 400 error is returned. + + If the specified course_id does not correspond to a valid course or if all the specified + usernames do not correspond to valid users, an HTTP 200 "OK" response is returned with an + empty 'results' field. + parameters: + - name: cursor + required: false + in: query + description: The pagination cursor value. + schema: + type: string + - name: page_size + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + tags: + - v1 + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCourseEnrollmentsApiListList' + description: '' + /v1/roles/: + get: + operationId: v1_roles_retrieve + description: Gets a list of all roles for the currently logged-in user, filtered + by course_id if supplied + tags: + - v1 + responses: + '200': + description: No response body + /v1/unenroll/: + post: + operationId: v1_unenroll_create + description: Unenrolls the specified user from all courses. + tags: + - v1 + responses: + '200': + description: No response body + /v2/course/{course_id}: + get: + operationId: v2_course_retrieve + description: Returns the course schedule and supported enrollment modes. No + authentication required. Use ?include_expired=1 to include expired enrollment + modes. + summary: Get enrollment details for a course + parameters: + - in: path + name: course_id + schema: + type: string + description: Course ID (e.g. course-v1:org+course+run). + required: true + - in: query + name: include_expired + schema: + type: string + description: If '1', include expired enrollment modes in the response. + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/enrollment.Course' + description: Course enrollment details retrieved successfully. + '400': + description: Invalid request data or parameters. + '404': + description: The requested resource does not exist. + /v2/enrollment/: + get: + operationId: v2_enrollment_list + description: Returns a paginated list of enrollments for the currently logged-in + user, or for the user named by the 'user' query parameter. Staff/admin/api-key + access is required to view another user's enrollments — otherwise the list + is filtered to courses the requester staffs. Supports the ADR 0036 ``?view=minimal`` + preset (see parameter description). + summary: List enrollments for a user (paginated) + parameters: + - in: query + name: page + schema: + type: string + description: Page number to retrieve. Default 1. + - in: query + name: page_size + schema: + type: string + description: Items per page (default 10, max 100). + - in: query + name: user + schema: + type: string + description: Username of the user whose enrollments to list. + - in: query + name: view + schema: + type: string + enum: + - minimal + description: ADR 0036 response preset. ``minimal`` collapses the embedded + ``course_details`` sub-object on each enrollment to a single ``course_id`` + string (drops ``course_modes`` and other heavy course-detail fields). Omit + the parameter to receive the full response. + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCourseEnrollmentList' + description: Paginated enrollment list. The schema below is the full default + shape; when ``?view=minimal`` is supplied each enrollment's ``course_details`` + is collapsed to a single ``course_id`` string (ADR 0036). + '401': + description: The requester is not authenticated. + post: + operationId: v2_enrollment_create + description: Enrolls a user in a course. Server-to-server calls may deactivate + or modify the mode of existing enrollments; all other requests create or reactivate + enrollments. The request body must include course_details.course_id. + summary: Create or update an enrollment + tags: + - openedx-platform-sdk + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollment' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CourseEnrollment' + multipart/form-data: + schema: + $ref: '#/components/schemas/CourseEnrollment' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollment' + description: Enrollment created, reactivated, or updated successfully. + '400': + description: Invalid request data or parameters. + '403': + description: The requester does not have permission for this operation. + '404': + description: The requested resource does not exist. + /v2/enrollment/{course_id}: + get: + operationId: v2_enrollment_retrieve + description: Returns the current user's enrollment for the specified course, + or the named user's enrollment when invoked with the {username},{course_id} + URL form (server-to-server or staff only). + summary: Retrieve a user's enrollment in a course + parameters: + - in: path + name: course_id + schema: + type: string + description: Course ID (e.g. course-v1:org+course+run). + required: true + - in: path + name: username + schema: + type: string + description: Username of the user. + required: true + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollment' + description: Enrollment retrieved successfully (or empty body if no enrollment). + '400': + description: Invalid request data or parameters. + '404': + description: The requested resource does not exist. + /v2/enrollment/{username},{course_id}: + get: + operationId: v2_enrollment_,_retrieve + description: Returns the current user's enrollment for the specified course, + or the named user's enrollment when invoked with the {username},{course_id} + URL form (server-to-server or staff only). + summary: Retrieve a user's enrollment in a course + parameters: + - in: path + name: course_id + schema: + type: string + description: Course ID (e.g. course-v1:org+course+run). + required: true + - in: path + name: username + schema: + type: string + description: Username of the user. + required: true + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollment' + description: Enrollment retrieved successfully (or empty body if no enrollment). + '400': + description: Invalid request data or parameters. + '404': + description: The requested resource does not exist. + /v2/enrollment/enrollment_allowed/: + get: + operationId: v2_enrollment_enrollment_allowed_retrieve + description: GET lists allowed enrollments for an email; POST creates a new + one; DELETE removes an existing one by email + course_id. Admin-only. + summary: Manage CourseEnrollmentAllowed records (admin-only) + parameters: + - in: query + name: email + schema: + type: string + description: Email to query (GET only). Defaults to the requester's email. + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCourseEnrollmentAllowedList' + description: GET success — list of allowed enrollments for the email. + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + description: POST success — allowed enrollment created. + '204': + description: DELETE success — allowed enrollment deleted. + '400': + description: Invalid request data or parameters. + '403': + description: The requester does not have permission for this operation. + '404': + description: 'DELETE: allowed enrollment not found.' + '409': + description: 'POST: allowed enrollment already exists.' + post: + operationId: v2_enrollment_enrollment_allowed_create + description: GET lists allowed enrollments for an email; POST creates a new + one; DELETE removes an existing one by email + course_id. Admin-only. + summary: Manage CourseEnrollmentAllowed records (admin-only) + parameters: + - in: query + name: email + schema: + type: string + description: Email to query (GET only). Defaults to the requester's email. + tags: + - openedx-platform-sdk + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + multipart/form-data: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCourseEnrollmentAllowedList' + description: GET success — list of allowed enrollments for the email. + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + description: POST success — allowed enrollment created. + '204': + description: DELETE success — allowed enrollment deleted. + '400': + description: Invalid request data or parameters. + '403': + description: The requester does not have permission for this operation. + '404': + description: 'DELETE: allowed enrollment not found.' + '409': + description: 'POST: allowed enrollment already exists.' + delete: + operationId: v2_enrollment_enrollment_allowed_destroy + description: GET lists allowed enrollments for an email; POST creates a new + one; DELETE removes an existing one by email + course_id. Admin-only. + summary: Manage CourseEnrollmentAllowed records (admin-only) + parameters: + - in: query + name: email + schema: + type: string + description: Email to query (GET only). Defaults to the requester's email. + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCourseEnrollmentAllowedList' + description: GET success — list of allowed enrollments for the email. + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + description: POST success — allowed enrollment created. + '204': + description: DELETE success — allowed enrollment deleted. + '400': + description: Invalid request data or parameters. + '403': + description: The requester does not have permission for this operation. + '404': + description: 'DELETE: allowed enrollment not found.' + '409': + description: 'POST: allowed enrollment already exists.' + /v2/enrollment/unenroll/: + post: + operationId: v2_enrollment_unenroll_create + description: Privileged retirement-pipeline use only. Unenrolls the named user + from every active enrollment. The request must be made by a service user with + CanRetireUser permission, not the user being unenrolled. + summary: Unenroll a user from all courses (retirement) + tags: + - openedx-platform-sdk + requestBody: + content: + application/json: + schema: + type: object + properties: + username: + type: string + required: + - username + application/x-www-form-urlencoded: + schema: + type: object + properties: + username: + type: string + required: + - username + multipart/form-data: + schema: + type: object + properties: + username: + type: string + required: + - username + responses: + '200': + description: List of courses from which the user was unenrolled. + '204': + description: User has no active enrollments. + '400': + description: Invalid request data or parameters. + '404': + description: The requested resource does not exist. + /v2/enrollments/: + get: + operationId: v2_enrollments_list + description: Admin-only paginated list of CourseEnrollment records, optionally + filtered by course_key, course_keys, username, or email, and optionally ordered. + summary: List all course enrollments (admin-only, paginated) + parameters: + - in: query + name: course_id + schema: + type: string + description: Deprecated alias for 'course_key' (ADR 0033). Use 'course_key' + instead. + deprecated: true + - in: query + name: course_ids + schema: + type: string + description: Deprecated alias for 'course_keys' (ADR 0033). Use 'course_keys' + instead. + deprecated: true + - in: query + name: course_key + schema: + type: string + description: Filter to enrollments for this course (OEP-68). + - in: query + name: course_keys + schema: + type: string + description: Comma-separated list of course keys (OEP-68). + - in: query + name: email + schema: + type: string + description: Comma-separated list of emails. + - in: query + name: ordering + schema: + type: string + description: 'Order results by one of: created, -created, id, -id (ADR 0033 + §3).' + - in: query + name: page + schema: + type: string + description: Page number to retrieve. Default 1. + - in: query + name: page_size + schema: + type: string + description: Items per page (default 10, max 100). + - in: query + name: username + schema: + type: string + description: Comma-separated list of usernames. + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCourseEnrollmentsApiListList' + description: Paginated list of course enrollments. + '400': + description: Invalid request data or parameters. + '401': + description: The requester is not authenticated. + '403': + description: The requester does not have permission for this operation. + /v2/roles/: + get: + operationId: v2_roles_retrieve + description: Returns the list of course-level roles held by the currently logged-in + user, plus an is_staff flag. Optionally filters by course_key (or course_id, + deprecated). + summary: List the current user's course roles + parameters: + - in: query + name: course_id + schema: + type: string + description: Deprecated alias for 'course_key' (ADR 0033). Use 'course_key' + instead. + deprecated: true + - in: query + name: course_key + schema: + type: string + description: If provided, only roles for this course are returned (OEP-68). + tags: + - openedx-platform-sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserRolesResponse' + description: Roles retrieved successfully. + '400': + description: Invalid request data or parameters. +components: + schemas: + CourseEnrollment: + type: object + description: |- + Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + properties: + created: + type: string + format: date-time + readOnly: true + nullable: true + mode: + type: string + maxLength: 100 + is_active: + type: boolean + course_details: + $ref: '#/components/schemas/enrollment.Course' + user: + type: string + readOnly: true + required: + - course_details + - created + - user + CourseEnrollmentAllowed: + type: object + description: |- + Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + properties: + email: + type: string + maxLength: 255 + course_id: + type: string + maxLength: 255 + auto_enroll: + type: boolean + created: + type: string + format: date-time + readOnly: true + nullable: true + user: + type: integer + nullable: true + description: First user which enrolled in the specified course through the + specified e-mail. Once set, it won't change. + required: + - course_id + - created + - email + CourseEnrollmentsApiList: + type: object + description: |- + Serializes CourseEnrollment model and returns a subset of fields returned + by the CourseEnrollmentSerializer. + properties: + created: + type: string + format: date-time + readOnly: true + nullable: true + mode: + type: string + maxLength: 100 + is_active: + type: boolean + user: + type: string + readOnly: true + course_id: + type: string + required: + - course_id + - created + - user + PaginatedCourseEnrollmentAllowedList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/CourseEnrollmentAllowed' + PaginatedCourseEnrollmentList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/CourseEnrollment' + PaginatedCourseEnrollmentsApiListList: + type: object + required: + - results + properties: + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?cursor=cD00ODY%3D" + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?cursor=cj0xJnA9NDg3 + results: + type: array + items: + $ref: '#/components/schemas/CourseEnrollmentsApiList' + UserRole: + type: object + description: Serializes a single course-level role entry for a user (ADR 0025). + properties: + org: + type: string + course_id: + type: string + readOnly: true + role: + type: string + required: + - course_id + - org + - role + UserRolesResponse: + type: object + description: Serializes the full response payload for UserRolesViewSet (ADR + 0025). + properties: + roles: + type: array + items: + $ref: '#/components/schemas/UserRole' + is_staff: + type: boolean + required: + - is_staff + - roles + enrollment.Course: + type: object + description: Serialize a course block and related information. + properties: + course_id: + type: string + course_name: + type: string + enrollment_start: + type: string + format: date-time + enrollment_end: + type: string + format: date-time + course_start: + type: string + format: date-time + course_end: + type: string + format: date-time + invite_only: + type: boolean + course_modes: + type: string + readOnly: true + pacing_type: + type: string + readOnly: true + required: + - course_end + - course_id + - course_modes + - course_name + - course_start + - enrollment_end + - enrollment_start + - invite_only + - pacing_type +servers: +- url: http://localhost:18000 + description: Local diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_course_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_course_retrieve.py new file mode 100644 index 0000000..ddc3c31 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_course_retrieve.py @@ -0,0 +1,194 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.enrollment_course import EnrollmentCourse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + course_id: str, + *, + include_expired: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["include_expired"] = include_expired + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v2/course/{course_id}".format( + course_id=quote(str(course_id), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | EnrollmentCourse | None: + if response.status_code == 200: + response_200 = EnrollmentCourse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | EnrollmentCourse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + course_id: str, + *, + client: AuthenticatedClient | Client, + include_expired: str | Unset = UNSET, +) -> Response[Any | EnrollmentCourse]: + """Get enrollment details for a course + + Returns the course schedule and supported enrollment modes. No authentication required. Use + ?include_expired=1 to include expired enrollment modes. + + Args: + course_id (str): + include_expired (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | EnrollmentCourse] + """ + + kwargs = _get_kwargs( + course_id=course_id, + include_expired=include_expired, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + course_id: str, + *, + client: AuthenticatedClient | Client, + include_expired: str | Unset = UNSET, +) -> Any | EnrollmentCourse | None: + """Get enrollment details for a course + + Returns the course schedule and supported enrollment modes. No authentication required. Use + ?include_expired=1 to include expired enrollment modes. + + Args: + course_id (str): + include_expired (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | EnrollmentCourse + """ + + return sync_detailed( + course_id=course_id, + client=client, + include_expired=include_expired, + ).parsed + + +async def asyncio_detailed( + course_id: str, + *, + client: AuthenticatedClient | Client, + include_expired: str | Unset = UNSET, +) -> Response[Any | EnrollmentCourse]: + """Get enrollment details for a course + + Returns the course schedule and supported enrollment modes. No authentication required. Use + ?include_expired=1 to include expired enrollment modes. + + Args: + course_id (str): + include_expired (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | EnrollmentCourse] + """ + + kwargs = _get_kwargs( + course_id=course_id, + include_expired=include_expired, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + course_id: str, + *, + client: AuthenticatedClient | Client, + include_expired: str | Unset = UNSET, +) -> Any | EnrollmentCourse | None: + """Get enrollment details for a course + + Returns the course schedule and supported enrollment modes. No authentication required. Use + ?include_expired=1 to include expired enrollment modes. + + Args: + course_id (str): + include_expired (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | EnrollmentCourse + """ + + return ( + await asyncio_detailed( + course_id=course_id, + client=client, + include_expired=include_expired, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_create.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_create.py new file mode 100644 index 0000000..ff9d53a --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_create.py @@ -0,0 +1,229 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_enrollment import CourseEnrollment +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: CourseEnrollment | CourseEnrollment | CourseEnrollment | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v2/enrollment/", + } + + if isinstance(body, CourseEnrollment): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | CourseEnrollment | None: + if response.status_code == 200: + response_200 = CourseEnrollment.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | CourseEnrollment]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollment | CourseEnrollment | CourseEnrollment | Unset = UNSET, +) -> Response[Any | CourseEnrollment]: + """Create or update an enrollment + + Enrolls a user in a course. Server-to-server calls may deactivate or modify the mode of existing + enrollments; all other requests create or reactivate enrollments. The request body must include + course_details.course_id. + + Args: + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollment] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollment | CourseEnrollment | CourseEnrollment | Unset = UNSET, +) -> Any | CourseEnrollment | None: + """Create or update an enrollment + + Enrolls a user in a course. Server-to-server calls may deactivate or modify the mode of existing + enrollments; all other requests create or reactivate enrollments. The request body must include + course_details.course_id. + + Args: + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollment + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollment | CourseEnrollment | CourseEnrollment | Unset = UNSET, +) -> Response[Any | CourseEnrollment]: + """Create or update an enrollment + + Enrolls a user in a course. Server-to-server calls may deactivate or modify the mode of existing + enrollments; all other requests create or reactivate enrollments. The request body must include + course_details.course_id. + + Args: + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollment] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollment | CourseEnrollment | CourseEnrollment | Unset = UNSET, +) -> Any | CourseEnrollment | None: + """Create or update an enrollment + + Enrolls a user in a course. Server-to-server calls may deactivate or modify the mode of existing + enrollments; all other requests create or reactivate enrollments. The request body must include + course_details.course_id. + + Args: + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + body (CourseEnrollment): Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollment + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_create.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_create.py new file mode 100644 index 0000000..4c614cb --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_create.py @@ -0,0 +1,261 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_enrollment_allowed import CourseEnrollmentAllowed +from ...models.paginated_course_enrollment_allowed_list import PaginatedCourseEnrollmentAllowedList +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: CourseEnrollmentAllowed | CourseEnrollmentAllowed | CourseEnrollmentAllowed | Unset = UNSET, + email: str | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["email"] = email + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v2/enrollment/enrollment_allowed/", + "params": params, + } + + if isinstance(body, CourseEnrollmentAllowed): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + if response.status_code == 200: + response_200 = PaginatedCourseEnrollmentAllowedList.from_dict(response.json()) + + return response_200 + + if response.status_code == 201: + response_201 = CourseEnrollmentAllowed.from_dict(response.json()) + + return response_201 + + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if response.status_code == 409: + response_409 = cast(Any, None) + return response_409 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollmentAllowed | CourseEnrollmentAllowed | CourseEnrollmentAllowed | Unset = UNSET, + email: str | Unset = UNSET, +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList] + """ + + kwargs = _get_kwargs( + body=body, + email=email, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollmentAllowed | CourseEnrollmentAllowed | CourseEnrollmentAllowed | Unset = UNSET, + email: str | Unset = UNSET, +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList + """ + + return sync_detailed( + client=client, + body=body, + email=email, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollmentAllowed | CourseEnrollmentAllowed | CourseEnrollmentAllowed | Unset = UNSET, + email: str | Unset = UNSET, +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList] + """ + + kwargs = _get_kwargs( + body=body, + email=email, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: CourseEnrollmentAllowed | CourseEnrollmentAllowed | CourseEnrollmentAllowed | Unset = UNSET, + email: str | Unset = UNSET, +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + body (CourseEnrollmentAllowed): Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + email=email, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_destroy.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_destroy.py new file mode 100644 index 0000000..d5acd10 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_destroy.py @@ -0,0 +1,198 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_enrollment_allowed import CourseEnrollmentAllowed +from ...models.paginated_course_enrollment_allowed_list import PaginatedCourseEnrollmentAllowedList +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + email: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["email"] = email + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/v2/enrollment/enrollment_allowed/", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + if response.status_code == 200: + response_200 = PaginatedCourseEnrollmentAllowedList.from_dict(response.json()) + + return response_200 + + if response.status_code == 201: + response_201 = CourseEnrollmentAllowed.from_dict(response.json()) + + return response_201 + + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if response.status_code == 409: + response_409 = cast(Any, None) + return response_409 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList] + """ + + kwargs = _get_kwargs( + email=email, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList + """ + + return sync_detailed( + client=client, + email=email, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList] + """ + + kwargs = _get_kwargs( + email=email, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList + """ + + return ( + await asyncio_detailed( + client=client, + email=email, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_retrieve.py new file mode 100644 index 0000000..20f39b5 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_enrollment_allowed_retrieve.py @@ -0,0 +1,198 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_enrollment_allowed import CourseEnrollmentAllowed +from ...models.paginated_course_enrollment_allowed_list import PaginatedCourseEnrollmentAllowedList +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + email: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["email"] = email + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v2/enrollment/enrollment_allowed/", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + if response.status_code == 200: + response_200 = PaginatedCourseEnrollmentAllowedList.from_dict(response.json()) + + return response_200 + + if response.status_code == 201: + response_201 = CourseEnrollmentAllowed.from_dict(response.json()) + + return response_201 + + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if response.status_code == 409: + response_409 = cast(Any, None) + return response_409 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList] + """ + + kwargs = _get_kwargs( + email=email, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList + """ + + return sync_detailed( + client=client, + email=email, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList]: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList] + """ + + kwargs = _get_kwargs( + email=email, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + email: str | Unset = UNSET, +) -> Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList | None: + """Manage CourseEnrollmentAllowed records (admin-only) + + GET lists allowed enrollments for an email; POST creates a new one; DELETE removes an existing one + by email + course_id. Admin-only. + + Args: + email (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollmentAllowed | PaginatedCourseEnrollmentAllowedList + """ + + return ( + await asyncio_detailed( + client=client, + email=email, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_list.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_list.py new file mode 100644 index 0000000..6c6d3a9 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_list.py @@ -0,0 +1,234 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.paginated_course_enrollment_list import PaginatedCourseEnrollmentList +from ...models.v2_enrollment_list_view import V2EnrollmentListView +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + user: str | Unset = UNSET, + view: V2EnrollmentListView | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["page"] = page + + params["page_size"] = page_size + + params["user"] = user + + json_view: str | Unset = UNSET + if not isinstance(view, Unset): + json_view = view.value + + params["view"] = json_view + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v2/enrollment/", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | PaginatedCourseEnrollmentList | None: + if response.status_code == 200: + response_200 = PaginatedCourseEnrollmentList.from_dict(response.json()) + + return response_200 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | PaginatedCourseEnrollmentList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + user: str | Unset = UNSET, + view: V2EnrollmentListView | Unset = UNSET, +) -> Response[Any | PaginatedCourseEnrollmentList]: + """List enrollments for a user (paginated) + + Returns a paginated list of enrollments for the currently logged-in user, or for the user named by + the 'user' query parameter. Staff/admin/api-key access is required to view another user's + enrollments — otherwise the list is filtered to courses the requester staffs. Supports the ADR 0036 + ``?view=minimal`` preset (see parameter description). + + Args: + page (str | Unset): + page_size (str | Unset): + user (str | Unset): + view (V2EnrollmentListView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | PaginatedCourseEnrollmentList] + """ + + kwargs = _get_kwargs( + page=page, + page_size=page_size, + user=user, + view=view, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + user: str | Unset = UNSET, + view: V2EnrollmentListView | Unset = UNSET, +) -> Any | PaginatedCourseEnrollmentList | None: + """List enrollments for a user (paginated) + + Returns a paginated list of enrollments for the currently logged-in user, or for the user named by + the 'user' query parameter. Staff/admin/api-key access is required to view another user's + enrollments — otherwise the list is filtered to courses the requester staffs. Supports the ADR 0036 + ``?view=minimal`` preset (see parameter description). + + Args: + page (str | Unset): + page_size (str | Unset): + user (str | Unset): + view (V2EnrollmentListView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | PaginatedCourseEnrollmentList + """ + + return sync_detailed( + client=client, + page=page, + page_size=page_size, + user=user, + view=view, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + user: str | Unset = UNSET, + view: V2EnrollmentListView | Unset = UNSET, +) -> Response[Any | PaginatedCourseEnrollmentList]: + """List enrollments for a user (paginated) + + Returns a paginated list of enrollments for the currently logged-in user, or for the user named by + the 'user' query parameter. Staff/admin/api-key access is required to view another user's + enrollments — otherwise the list is filtered to courses the requester staffs. Supports the ADR 0036 + ``?view=minimal`` preset (see parameter description). + + Args: + page (str | Unset): + page_size (str | Unset): + user (str | Unset): + view (V2EnrollmentListView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | PaginatedCourseEnrollmentList] + """ + + kwargs = _get_kwargs( + page=page, + page_size=page_size, + user=user, + view=view, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + user: str | Unset = UNSET, + view: V2EnrollmentListView | Unset = UNSET, +) -> Any | PaginatedCourseEnrollmentList | None: + """List enrollments for a user (paginated) + + Returns a paginated list of enrollments for the currently logged-in user, or for the user named by + the 'user' query parameter. Staff/admin/api-key access is required to view another user's + enrollments — otherwise the list is filtered to courses the requester staffs. Supports the ADR 0036 + ``?view=minimal`` preset (see parameter description). + + Args: + page (str | Unset): + page_size (str | Unset): + user (str | Unset): + view (V2EnrollmentListView | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | PaginatedCourseEnrollmentList + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + page_size=page_size, + user=user, + view=view, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_retrieve.py new file mode 100644 index 0000000..a1d8343 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_retrieve.py @@ -0,0 +1,187 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.course_enrollment import CourseEnrollment +from ...types import Response + + +def _get_kwargs( + username: str, + course_id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v2/enrollment/{username},{course_id}".format( + username=quote(str(username), safe=""), + course_id=quote(str(course_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | CourseEnrollment | None: + if response.status_code == 200: + response_200 = CourseEnrollment.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | CourseEnrollment]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + username: str, + course_id: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Any | CourseEnrollment]: + """Retrieve a user's enrollment in a course + + Returns the current user's enrollment for the specified course, or the named user's enrollment when + invoked with the {username},{course_id} URL form (server-to-server or staff only). + + Args: + username (str): + course_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollment] + """ + + kwargs = _get_kwargs( + username=username, + course_id=course_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + username: str, + course_id: str, + *, + client: AuthenticatedClient | Client, +) -> Any | CourseEnrollment | None: + """Retrieve a user's enrollment in a course + + Returns the current user's enrollment for the specified course, or the named user's enrollment when + invoked with the {username},{course_id} URL form (server-to-server or staff only). + + Args: + username (str): + course_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollment + """ + + return sync_detailed( + username=username, + course_id=course_id, + client=client, + ).parsed + + +async def asyncio_detailed( + username: str, + course_id: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Any | CourseEnrollment]: + """Retrieve a user's enrollment in a course + + Returns the current user's enrollment for the specified course, or the named user's enrollment when + invoked with the {username},{course_id} URL form (server-to-server or staff only). + + Args: + username (str): + course_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | CourseEnrollment] + """ + + kwargs = _get_kwargs( + username=username, + course_id=course_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + username: str, + course_id: str, + *, + client: AuthenticatedClient | Client, +) -> Any | CourseEnrollment | None: + """Retrieve a user's enrollment in a course + + Returns the current user's enrollment for the specified course, or the named user's enrollment when + invoked with the {username},{course_id} URL form (server-to-server or staff only). + + Args: + username (str): + course_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | CourseEnrollment + """ + + return ( + await asyncio_detailed( + username=username, + course_id=course_id, + client=client, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_unenroll_create.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_unenroll_create.py new file mode 100644 index 0000000..8672982 --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollment_unenroll_create.py @@ -0,0 +1,134 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.v2_enrollment_unenroll_create_data_body import V2EnrollmentUnenrollCreateDataBody +from ...models.v2_enrollment_unenroll_create_files_body import V2EnrollmentUnenrollCreateFilesBody +from ...models.v2_enrollment_unenroll_create_json_body import V2EnrollmentUnenrollCreateJsonBody +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: V2EnrollmentUnenrollCreateJsonBody + | V2EnrollmentUnenrollCreateDataBody + | V2EnrollmentUnenrollCreateFilesBody + | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v2/enrollment/unenroll/", + } + + if isinstance(body, V2EnrollmentUnenrollCreateJsonBody): + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: + if response.status_code == 200: + return None + + if response.status_code == 204: + return None + + if response.status_code == 400: + return None + + if response.status_code == 404: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: V2EnrollmentUnenrollCreateJsonBody + | V2EnrollmentUnenrollCreateDataBody + | V2EnrollmentUnenrollCreateFilesBody + | Unset = UNSET, +) -> Response[Any]: + """Unenroll a user from all courses (retirement) + + Privileged retirement-pipeline use only. Unenrolls the named user from every active enrollment. The + request must be made by a service user with CanRetireUser permission, not the user being unenrolled. + + Args: + body (V2EnrollmentUnenrollCreateJsonBody | Unset): + body (V2EnrollmentUnenrollCreateDataBody | Unset): + body (V2EnrollmentUnenrollCreateFilesBody | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: V2EnrollmentUnenrollCreateJsonBody + | V2EnrollmentUnenrollCreateDataBody + | V2EnrollmentUnenrollCreateFilesBody + | Unset = UNSET, +) -> Response[Any]: + """Unenroll a user from all courses (retirement) + + Privileged retirement-pipeline use only. Unenrolls the named user from every active enrollment. The + request must be made by a service user with CanRetireUser permission, not the user being unenrolled. + + Args: + body (V2EnrollmentUnenrollCreateJsonBody | Unset): + body (V2EnrollmentUnenrollCreateDataBody | Unset): + body (V2EnrollmentUnenrollCreateFilesBody | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollments_list.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollments_list.py new file mode 100644 index 0000000..9d0905a --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_enrollments_list.py @@ -0,0 +1,304 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.paginated_course_enrollments_api_list_list import PaginatedCourseEnrollmentsApiListList +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + course_id: str | Unset = UNSET, + course_ids: str | Unset = UNSET, + course_key: str | Unset = UNSET, + course_keys: str | Unset = UNSET, + email: str | Unset = UNSET, + ordering: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + username: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["course_id"] = course_id + + params["course_ids"] = course_ids + + params["course_key"] = course_key + + params["course_keys"] = course_keys + + params["email"] = email + + params["ordering"] = ordering + + params["page"] = page + + params["page_size"] = page_size + + params["username"] = username + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v2/enrollments/", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | PaginatedCourseEnrollmentsApiListList | None: + if response.status_code == 200: + response_200 = PaginatedCourseEnrollmentsApiListList.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + + if response.status_code == 403: + response_403 = cast(Any, None) + return response_403 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | PaginatedCourseEnrollmentsApiListList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_ids: str | Unset = UNSET, + course_key: str | Unset = UNSET, + course_keys: str | Unset = UNSET, + email: str | Unset = UNSET, + ordering: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + username: str | Unset = UNSET, +) -> Response[Any | PaginatedCourseEnrollmentsApiListList]: + """List all course enrollments (admin-only, paginated) + + Admin-only paginated list of CourseEnrollment records, optionally filtered by course_key, + course_keys, username, or email, and optionally ordered. + + Args: + course_id (str | Unset): + course_ids (str | Unset): + course_key (str | Unset): + course_keys (str | Unset): + email (str | Unset): + ordering (str | Unset): + page (str | Unset): + page_size (str | Unset): + username (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | PaginatedCourseEnrollmentsApiListList] + """ + + kwargs = _get_kwargs( + course_id=course_id, + course_ids=course_ids, + course_key=course_key, + course_keys=course_keys, + email=email, + ordering=ordering, + page=page, + page_size=page_size, + username=username, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_ids: str | Unset = UNSET, + course_key: str | Unset = UNSET, + course_keys: str | Unset = UNSET, + email: str | Unset = UNSET, + ordering: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + username: str | Unset = UNSET, +) -> Any | PaginatedCourseEnrollmentsApiListList | None: + """List all course enrollments (admin-only, paginated) + + Admin-only paginated list of CourseEnrollment records, optionally filtered by course_key, + course_keys, username, or email, and optionally ordered. + + Args: + course_id (str | Unset): + course_ids (str | Unset): + course_key (str | Unset): + course_keys (str | Unset): + email (str | Unset): + ordering (str | Unset): + page (str | Unset): + page_size (str | Unset): + username (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | PaginatedCourseEnrollmentsApiListList + """ + + return sync_detailed( + client=client, + course_id=course_id, + course_ids=course_ids, + course_key=course_key, + course_keys=course_keys, + email=email, + ordering=ordering, + page=page, + page_size=page_size, + username=username, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_ids: str | Unset = UNSET, + course_key: str | Unset = UNSET, + course_keys: str | Unset = UNSET, + email: str | Unset = UNSET, + ordering: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + username: str | Unset = UNSET, +) -> Response[Any | PaginatedCourseEnrollmentsApiListList]: + """List all course enrollments (admin-only, paginated) + + Admin-only paginated list of CourseEnrollment records, optionally filtered by course_key, + course_keys, username, or email, and optionally ordered. + + Args: + course_id (str | Unset): + course_ids (str | Unset): + course_key (str | Unset): + course_keys (str | Unset): + email (str | Unset): + ordering (str | Unset): + page (str | Unset): + page_size (str | Unset): + username (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | PaginatedCourseEnrollmentsApiListList] + """ + + kwargs = _get_kwargs( + course_id=course_id, + course_ids=course_ids, + course_key=course_key, + course_keys=course_keys, + email=email, + ordering=ordering, + page=page, + page_size=page_size, + username=username, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_ids: str | Unset = UNSET, + course_key: str | Unset = UNSET, + course_keys: str | Unset = UNSET, + email: str | Unset = UNSET, + ordering: str | Unset = UNSET, + page: str | Unset = UNSET, + page_size: str | Unset = UNSET, + username: str | Unset = UNSET, +) -> Any | PaginatedCourseEnrollmentsApiListList | None: + """List all course enrollments (admin-only, paginated) + + Admin-only paginated list of CourseEnrollment records, optionally filtered by course_key, + course_keys, username, or email, and optionally ordered. + + Args: + course_id (str | Unset): + course_ids (str | Unset): + course_key (str | Unset): + course_keys (str | Unset): + email (str | Unset): + ordering (str | Unset): + page (str | Unset): + page_size (str | Unset): + username (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | PaginatedCourseEnrollmentsApiListList + """ + + return ( + await asyncio_detailed( + client=client, + course_id=course_id, + course_ids=course_ids, + course_key=course_key, + course_keys=course_keys, + email=email, + ordering=ordering, + page=page, + page_size=page_size, + username=username, + ) + ).parsed diff --git a/openedx_platform_sdk/api/openedx_platform_sdk/v2_roles_retrieve.py b/openedx_platform_sdk/api/openedx_platform_sdk/v2_roles_retrieve.py new file mode 100644 index 0000000..0bfd4ec --- /dev/null +++ b/openedx_platform_sdk/api/openedx_platform_sdk/v2_roles_retrieve.py @@ -0,0 +1,191 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.user_roles_response import UserRolesResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + course_id: str | Unset = UNSET, + course_key: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["course_id"] = course_id + + params["course_key"] = course_key + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v2/roles/", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | UserRolesResponse | None: + if response.status_code == 200: + response_200 = UserRolesResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = cast(Any, None) + return response_400 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | UserRolesResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_key: str | Unset = UNSET, +) -> Response[Any | UserRolesResponse]: + """List the current user's course roles + + Returns the list of course-level roles held by the currently logged-in user, plus an is_staff flag. + Optionally filters by course_key (or course_id, deprecated). + + Args: + course_id (str | Unset): + course_key (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | UserRolesResponse] + """ + + kwargs = _get_kwargs( + course_id=course_id, + course_key=course_key, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_key: str | Unset = UNSET, +) -> Any | UserRolesResponse | None: + """List the current user's course roles + + Returns the list of course-level roles held by the currently logged-in user, plus an is_staff flag. + Optionally filters by course_key (or course_id, deprecated). + + Args: + course_id (str | Unset): + course_key (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | UserRolesResponse + """ + + return sync_detailed( + client=client, + course_id=course_id, + course_key=course_key, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_key: str | Unset = UNSET, +) -> Response[Any | UserRolesResponse]: + """List the current user's course roles + + Returns the list of course-level roles held by the currently logged-in user, plus an is_staff flag. + Optionally filters by course_key (or course_id, deprecated). + + Args: + course_id (str | Unset): + course_key (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | UserRolesResponse] + """ + + kwargs = _get_kwargs( + course_id=course_id, + course_key=course_key, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + course_id: str | Unset = UNSET, + course_key: str | Unset = UNSET, +) -> Any | UserRolesResponse | None: + """List the current user's course roles + + Returns the list of course-level roles held by the currently logged-in user, plus an is_staff flag. + Optionally filters by course_key (or course_id, deprecated). + + Args: + course_id (str | Unset): + course_key (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | UserRolesResponse + """ + + return ( + await asyncio_detailed( + client=client, + course_id=course_id, + course_key=course_key, + ) + ).parsed diff --git a/openedx_platform_sdk/auth.py b/openedx_platform_sdk/auth.py index fe003d6..9300654 100644 --- a/openedx_platform_sdk/auth.py +++ b/openedx_platform_sdk/auth.py @@ -24,7 +24,6 @@ """ import time -from typing import Optional import httpx @@ -61,7 +60,7 @@ def __init__( self.refresh_buffer_seconds = refresh_buffer_seconds self.verify_ssl = verify_ssl - self._token: Optional[str] = None + self._token: str | None = None self._expires_at: float = 0.0 def _fetch_token(self) -> None: @@ -91,7 +90,7 @@ def get_client( self, studio_url: str, raise_on_unexpected_status: bool = False, - verify_ssl: Optional[bool] = None, + verify_ssl: bool | None = None, **kwargs, ) -> AuthenticatedClient: """ diff --git a/openedx_platform_sdk/models/__init__.py b/openedx_platform_sdk/models/__init__.py index 5d518fa..994e8f5 100644 --- a/openedx_platform_sdk/models/__init__.py +++ b/openedx_platform_sdk/models/__init__.py @@ -7,12 +7,19 @@ from .course_common import CourseCommon from .course_common_serializer_v2 import CourseCommonSerializerV2 from .course_details import CourseDetails +from .course_enrollment import CourseEnrollment +from .course_enrollment_allowed import CourseEnrollmentAllowed +from .course_enrollments_api_list import CourseEnrollmentsApiList from .course_home_tab import CourseHomeTab from .course_home_tab_serializer_v2 import CourseHomeTabSerializerV2 +from .enrollment_course import EnrollmentCourse from .instructor_info import InstructorInfo from .instructors import Instructors from .library_tab import LibraryTab from .library_view import LibraryView +from .paginated_course_enrollment_allowed_list import PaginatedCourseEnrollmentAllowedList +from .paginated_course_enrollment_list import PaginatedCourseEnrollmentList +from .paginated_course_enrollments_api_list_list import PaginatedCourseEnrollmentsApiListList from .paginated_v4_home_courses_response import PaginatedV4HomeCoursesResponse from .patched_xblock import PatchedXblock from .patched_xblock_actions_type_0 import PatchedXblockActionsType0 @@ -26,7 +33,13 @@ from .studio_home import StudioHome from .unsucceeded_course import UnsucceededCourse from .unsucceeded_course_serializer_v2 import UnsucceededCourseSerializerV2 +from .user_role import UserRole +from .user_roles_response import UserRolesResponse from .v1_xblock_retrieve_view import V1XblockRetrieveView +from .v2_enrollment_list_view import V2EnrollmentListView +from .v2_enrollment_unenroll_create_data_body import V2EnrollmentUnenrollCreateDataBody +from .v2_enrollment_unenroll_create_files_body import V2EnrollmentUnenrollCreateFilesBody +from .v2_enrollment_unenroll_create_json_body import V2EnrollmentUnenrollCreateJsonBody from .v3_course_details_retrieve_view import V3CourseDetailsRetrieveView from .xblock import Xblock from .xblock_actions_type_0 import XblockActionsType0 @@ -43,12 +56,19 @@ "CourseCommon", "CourseCommonSerializerV2", "CourseDetails", + "CourseEnrollment", + "CourseEnrollmentAllowed", + "CourseEnrollmentsApiList", "CourseHomeTab", "CourseHomeTabSerializerV2", + "EnrollmentCourse", "InstructorInfo", "Instructors", "LibraryTab", "LibraryView", + "PaginatedCourseEnrollmentAllowedList", + "PaginatedCourseEnrollmentList", + "PaginatedCourseEnrollmentsApiListList", "PaginatedV4HomeCoursesResponse", "PatchedauthoringGradingCourseGradingV0", "PatchedauthoringGradingCourseGradingV0GradeCutoffs", @@ -62,7 +82,13 @@ "StudioHome", "UnsucceededCourse", "UnsucceededCourseSerializerV2", + "UserRole", + "UserRolesResponse", "V1XblockRetrieveView", + "V2EnrollmentListView", + "V2EnrollmentUnenrollCreateDataBody", + "V2EnrollmentUnenrollCreateFilesBody", + "V2EnrollmentUnenrollCreateJsonBody", "V3CourseDetailsRetrieveView", "Xblock", "XblockActionsType0", diff --git a/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py b/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py index 57846a8..e4e7573 100644 --- a/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py +++ b/openedx_platform_sdk/models/authoring_grading_course_grading_v0.py @@ -51,7 +51,7 @@ def to_dict(self) -> dict[str, Any]: grade_cutoffs: dict[str, Any] | Unset = UNSET if not isinstance(self.grade_cutoffs, Unset): - grade_cutoffs = self.grade_cutoffs.to_dict() + grade_cutoffs = self.grade_cutoffs.to_dict() if not isinstance(self.grade_cutoffs, dict) else dict(self.grade_cutoffs) grace_period: dict[str, Any] | None | Unset if isinstance(self.grace_period, Unset): diff --git a/openedx_platform_sdk/models/course_details.py b/openedx_platform_sdk/models/course_details.py index 9c22cab..4f7490e 100644 --- a/openedx_platform_sdk/models/course_details.py +++ b/openedx_platform_sdk/models/course_details.py @@ -526,7 +526,8 @@ def _parse_license_(data: object) -> None | str: short_description = d.pop("short_description") - start_date = datetime.datetime.fromisoformat(d.pop("start_date")) + _raw_start_date = d.pop("start_date") + start_date = datetime.datetime.fromisoformat(_raw_start_date) if isinstance(_raw_start_date, str) else None subtitle = d.pop("subtitle") diff --git a/openedx_platform_sdk/models/course_enrollment.py b/openedx_platform_sdk/models/course_enrollment.py new file mode 100644 index 0000000..2fb44de --- /dev/null +++ b/openedx_platform_sdk/models/course_enrollment.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import datetime +import json +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.enrollment_course import EnrollmentCourse + + +T = TypeVar("T", bound="CourseEnrollment") + + +@_attrs_define +class CourseEnrollment: + """Serializes CourseEnrollment models + + Aggregates all data from the Course Enrollment table, and pulls in the serialization for + the Course block and course modes, to give a complete representation of course enrollment. + + Attributes: + created (datetime.datetime | None): + course_details (EnrollmentCourse): Serialize a course block and related information. + user (str): + mode (str | Unset): + is_active (bool | Unset): + """ + + created: datetime.datetime | None + course_details: EnrollmentCourse + user: str + mode: str | Unset = UNSET + is_active: bool | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + created: None | str + if isinstance(self.created, datetime.datetime): + created = self.created.isoformat() + else: + created = self.created + + course_details = self.course_details.to_dict() + + user = self.user + + mode = self.mode + + is_active = self.is_active + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "created": created, + "course_details": course_details, + "user": user, + } + ) + if mode is not UNSET: + field_dict["mode"] = mode + if is_active is not UNSET: + field_dict["is_active"] = is_active + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + if isinstance(self.created, datetime.datetime): + files.append(("created", (None, self.created.isoformat().encode(), "text/plain"))) + else: + files.append(("created", (None, str(self.created).encode(), "text/plain"))) + + files.append(("course_details", (None, json.dumps(self.course_details.to_dict()).encode(), "application/json"))) + + files.append(("user", (None, str(self.user).encode(), "text/plain"))) + + if not isinstance(self.mode, Unset): + files.append(("mode", (None, str(self.mode).encode(), "text/plain"))) + + if not isinstance(self.is_active, Unset): + files.append(("is_active", (None, str(self.is_active).encode(), "text/plain"))) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.enrollment_course import EnrollmentCourse + + d = dict(src_dict) + + def _parse_created(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + created_type_0 = datetime.datetime.fromisoformat(data) + + return created_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + created = _parse_created(d.pop("created")) + + course_details = EnrollmentCourse.from_dict(d.pop("course_details")) + + user = d.pop("user") + + mode = d.pop("mode", UNSET) + + is_active = d.pop("is_active", UNSET) + + course_enrollment = cls( + created=created, + course_details=course_details, + user=user, + mode=mode, + is_active=is_active, + ) + + course_enrollment.additional_properties = d + return course_enrollment + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_enrollment_allowed.py b/openedx_platform_sdk/models/course_enrollment_allowed.py new file mode 100644 index 0000000..888d5cd --- /dev/null +++ b/openedx_platform_sdk/models/course_enrollment_allowed.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CourseEnrollmentAllowed") + + +@_attrs_define +class CourseEnrollmentAllowed: + """Serializes CourseEnrollmentAllowed model + + Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization + to give a complete representation of course enrollment allowed. + + Attributes: + email (str): + course_id (str): + created (datetime.datetime | None): + auto_enroll (bool | Unset): + user (int | None | Unset): First user which enrolled in the specified course through the specified e-mail. Once + set, it won't change. + """ + + email: str + course_id: str + created: datetime.datetime | None + auto_enroll: bool | Unset = UNSET + user: int | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + email = self.email + + course_id = self.course_id + + created: None | str + if isinstance(self.created, datetime.datetime): + created = self.created.isoformat() + else: + created = self.created + + auto_enroll = self.auto_enroll + + user: int | None | Unset + if isinstance(self.user, Unset): + user = UNSET + else: + user = self.user + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "email": email, + "course_id": course_id, + "created": created, + } + ) + if auto_enroll is not UNSET: + field_dict["auto_enroll"] = auto_enroll + if user is not UNSET: + field_dict["user"] = user + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + files.append(("email", (None, str(self.email).encode(), "text/plain"))) + + files.append(("course_id", (None, str(self.course_id).encode(), "text/plain"))) + + if isinstance(self.created, datetime.datetime): + files.append(("created", (None, self.created.isoformat().encode(), "text/plain"))) + else: + files.append(("created", (None, str(self.created).encode(), "text/plain"))) + + if not isinstance(self.auto_enroll, Unset): + files.append(("auto_enroll", (None, str(self.auto_enroll).encode(), "text/plain"))) + + if not isinstance(self.user, Unset): + if isinstance(self.user, int): + files.append(("user", (None, str(self.user).encode(), "text/plain"))) + else: + files.append(("user", (None, str(self.user).encode(), "text/plain"))) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + email = d.pop("email") + + course_id = d.pop("course_id") + + def _parse_created(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + created_type_0 = datetime.datetime.fromisoformat(data) + + return created_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + created = _parse_created(d.pop("created")) + + auto_enroll = d.pop("auto_enroll", UNSET) + + def _parse_user(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + user = _parse_user(d.pop("user", UNSET)) + + course_enrollment_allowed = cls( + email=email, + course_id=course_id, + created=created, + auto_enroll=auto_enroll, + user=user, + ) + + course_enrollment_allowed.additional_properties = d + return course_enrollment_allowed + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/course_enrollments_api_list.py b/openedx_platform_sdk/models/course_enrollments_api_list.py new file mode 100644 index 0000000..2f48fd6 --- /dev/null +++ b/openedx_platform_sdk/models/course_enrollments_api_list.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CourseEnrollmentsApiList") + + +@_attrs_define +class CourseEnrollmentsApiList: + """Serializes CourseEnrollment model and returns a subset of fields returned + by the CourseEnrollmentSerializer. + + Attributes: + created (datetime.datetime | None): + user (str): + course_id (str): + mode (str | Unset): + is_active (bool | Unset): + """ + + created: datetime.datetime | None + user: str + course_id: str + mode: str | Unset = UNSET + is_active: bool | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + created: None | str + if isinstance(self.created, datetime.datetime): + created = self.created.isoformat() + else: + created = self.created + + user = self.user + + course_id = self.course_id + + mode = self.mode + + is_active = self.is_active + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "created": created, + "user": user, + "course_id": course_id, + } + ) + if mode is not UNSET: + field_dict["mode"] = mode + if is_active is not UNSET: + field_dict["is_active"] = is_active + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_created(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + created_type_0 = datetime.datetime.fromisoformat(data) + + return created_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + created = _parse_created(d.pop("created")) + + user = d.pop("user") + + course_id = d.pop("course_id") + + mode = d.pop("mode", UNSET) + + is_active = d.pop("is_active", UNSET) + + course_enrollments_api_list = cls( + created=created, + user=user, + course_id=course_id, + mode=mode, + is_active=is_active, + ) + + course_enrollments_api_list.additional_properties = d + return course_enrollments_api_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/enrollment_course.py b/openedx_platform_sdk/models/enrollment_course.py new file mode 100644 index 0000000..7e35c60 --- /dev/null +++ b/openedx_platform_sdk/models/enrollment_course.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="EnrollmentCourse") + + +@_attrs_define +class EnrollmentCourse: + """Serialize a course block and related information. + + Attributes: + course_id (str): + course_name (str): + enrollment_start (datetime.datetime): + enrollment_end (datetime.datetime): + course_start (datetime.datetime): + course_end (datetime.datetime): + invite_only (bool): + course_modes (str): + pacing_type (str): + """ + + course_id: str + course_name: str + enrollment_start: datetime.datetime + enrollment_end: datetime.datetime + course_start: datetime.datetime + course_end: datetime.datetime + invite_only: bool + course_modes: str + pacing_type: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + course_id = self.course_id + + course_name = self.course_name + + enrollment_start = self.enrollment_start.isoformat() + + enrollment_end = self.enrollment_end.isoformat() + + course_start = self.course_start.isoformat() + + course_end = self.course_end.isoformat() + + invite_only = self.invite_only + + course_modes = self.course_modes + + pacing_type = self.pacing_type + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "course_id": course_id, + "course_name": course_name, + "enrollment_start": enrollment_start, + "enrollment_end": enrollment_end, + "course_start": course_start, + "course_end": course_end, + "invite_only": invite_only, + "course_modes": course_modes, + "pacing_type": pacing_type, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + course_id = d.pop("course_id") + + course_name = d.pop("course_name") + + _raw_enrollment_start = d.pop("enrollment_start") + enrollment_start = datetime.datetime.fromisoformat(_raw_enrollment_start) if isinstance(_raw_enrollment_start, str) else None + + _raw_enrollment_end = d.pop("enrollment_end") + enrollment_end = datetime.datetime.fromisoformat(_raw_enrollment_end) if isinstance(_raw_enrollment_end, str) else None + + _raw_course_start = d.pop("course_start") + course_start = datetime.datetime.fromisoformat(_raw_course_start) if isinstance(_raw_course_start, str) else None + + _raw_course_end = d.pop("course_end") + course_end = datetime.datetime.fromisoformat(_raw_course_end) if isinstance(_raw_course_end, str) else None + + invite_only = d.pop("invite_only") + + course_modes = d.pop("course_modes") + + pacing_type = d.pop("pacing_type") + + enrollment_course = cls( + course_id=course_id, + course_name=course_name, + enrollment_start=enrollment_start, + enrollment_end=enrollment_end, + course_start=course_start, + course_end=course_end, + invite_only=invite_only, + course_modes=course_modes, + pacing_type=pacing_type, + ) + + enrollment_course.additional_properties = d + return enrollment_course + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/paginated_course_enrollment_allowed_list.py b/openedx_platform_sdk/models/paginated_course_enrollment_allowed_list.py new file mode 100644 index 0000000..4001f7f --- /dev/null +++ b/openedx_platform_sdk/models/paginated_course_enrollment_allowed_list.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.course_enrollment_allowed import CourseEnrollmentAllowed + + +T = TypeVar("T", bound="PaginatedCourseEnrollmentAllowedList") + + +@_attrs_define +class PaginatedCourseEnrollmentAllowedList: + """ + Attributes: + count (int): Example: 123. + results (list[CourseEnrollmentAllowed]): + next_ (None | str | Unset): Example: http://api.example.org/accounts/?page=4. + previous (None | str | Unset): Example: http://api.example.org/accounts/?page=2. + """ + + count: int + results: list[CourseEnrollmentAllowed] + next_: None | str | Unset = UNSET + previous: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + count = self.count + + results = [] + for results_item_data in self.results: + results_item = results_item_data.to_dict() + results.append(results_item) + + next_: None | str | Unset + if isinstance(self.next_, Unset): + next_ = UNSET + else: + next_ = self.next_ + + previous: None | str | Unset + if isinstance(self.previous, Unset): + previous = UNSET + else: + previous = self.previous + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "count": count, + "results": results, + } + ) + if next_ is not UNSET: + field_dict["next"] = next_ + if previous is not UNSET: + field_dict["previous"] = previous + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.course_enrollment_allowed import CourseEnrollmentAllowed + + if isinstance(src_dict, list): + src_dict = {"count": len(src_dict), "results": src_dict} + d = dict(src_dict) + count = d.pop("count") + + results = [] + _results = d.pop("results") + for results_item_data in _results: + results_item = CourseEnrollmentAllowed.from_dict(results_item_data) + + results.append(results_item) + + def _parse_next_(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + next_ = _parse_next_(d.pop("next", UNSET)) + + def _parse_previous(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + previous = _parse_previous(d.pop("previous", UNSET)) + + paginated_course_enrollment_allowed_list = cls( + count=count, + results=results, + next_=next_, + previous=previous, + ) + + paginated_course_enrollment_allowed_list.additional_properties = d + return paginated_course_enrollment_allowed_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/paginated_course_enrollment_list.py b/openedx_platform_sdk/models/paginated_course_enrollment_list.py new file mode 100644 index 0000000..923853d --- /dev/null +++ b/openedx_platform_sdk/models/paginated_course_enrollment_list.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.course_enrollment import CourseEnrollment + + +T = TypeVar("T", bound="PaginatedCourseEnrollmentList") + + +@_attrs_define +class PaginatedCourseEnrollmentList: + """ + Attributes: + count (int): Example: 123. + results (list[CourseEnrollment]): + next_ (None | str | Unset): Example: http://api.example.org/accounts/?page=4. + previous (None | str | Unset): Example: http://api.example.org/accounts/?page=2. + """ + + count: int + results: list[CourseEnrollment] + next_: None | str | Unset = UNSET + previous: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + count = self.count + + results = [] + for results_item_data in self.results: + results_item = results_item_data.to_dict() + results.append(results_item) + + next_: None | str | Unset + if isinstance(self.next_, Unset): + next_ = UNSET + else: + next_ = self.next_ + + previous: None | str | Unset + if isinstance(self.previous, Unset): + previous = UNSET + else: + previous = self.previous + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "count": count, + "results": results, + } + ) + if next_ is not UNSET: + field_dict["next"] = next_ + if previous is not UNSET: + field_dict["previous"] = previous + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.course_enrollment import CourseEnrollment + + d = dict(src_dict) + count = d.pop("count") + + results = [] + _results = d.pop("results") + for results_item_data in _results: + results_item = CourseEnrollment.from_dict(results_item_data) + + results.append(results_item) + + def _parse_next_(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + next_ = _parse_next_(d.pop("next", UNSET)) + + def _parse_previous(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + previous = _parse_previous(d.pop("previous", UNSET)) + + paginated_course_enrollment_list = cls( + count=count, + results=results, + next_=next_, + previous=previous, + ) + + paginated_course_enrollment_list.additional_properties = d + return paginated_course_enrollment_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/paginated_course_enrollments_api_list_list.py b/openedx_platform_sdk/models/paginated_course_enrollments_api_list_list.py new file mode 100644 index 0000000..e1b7405 --- /dev/null +++ b/openedx_platform_sdk/models/paginated_course_enrollments_api_list_list.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.course_enrollments_api_list import CourseEnrollmentsApiList + + +T = TypeVar("T", bound="PaginatedCourseEnrollmentsApiListList") + + +@_attrs_define +class PaginatedCourseEnrollmentsApiListList: + """ + Attributes: + results (list[CourseEnrollmentsApiList]): + next_ (None | str | Unset): Example: http://api.example.org/accounts/?cursor=cD00ODY%3D". + previous (None | str | Unset): Example: http://api.example.org/accounts/?cursor=cj0xJnA9NDg3. + """ + + results: list[CourseEnrollmentsApiList] + next_: None | str | Unset = UNSET + previous: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + results = [] + for results_item_data in self.results: + results_item = results_item_data.to_dict() + results.append(results_item) + + next_: None | str | Unset + if isinstance(self.next_, Unset): + next_ = UNSET + else: + next_ = self.next_ + + previous: None | str | Unset + if isinstance(self.previous, Unset): + previous = UNSET + else: + previous = self.previous + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "results": results, + } + ) + if next_ is not UNSET: + field_dict["next"] = next_ + if previous is not UNSET: + field_dict["previous"] = previous + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.course_enrollments_api_list import CourseEnrollmentsApiList + + d = dict(src_dict) + results = [] + _results = d.pop("results") + for results_item_data in _results: + results_item = CourseEnrollmentsApiList.from_dict(results_item_data) + + results.append(results_item) + + def _parse_next_(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + next_ = _parse_next_(d.pop("next", UNSET)) + + def _parse_previous(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + previous = _parse_previous(d.pop("previous", UNSET)) + + paginated_course_enrollments_api_list_list = cls( + results=results, + next_=next_, + previous=previous, + ) + + paginated_course_enrollments_api_list_list.additional_properties = d + return paginated_course_enrollments_api_list_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/user_role.py b/openedx_platform_sdk/models/user_role.py new file mode 100644 index 0000000..20fdebe --- /dev/null +++ b/openedx_platform_sdk/models/user_role.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UserRole") + + +@_attrs_define +class UserRole: + """Serializes a single course-level role entry for a user (ADR 0025). + + Attributes: + org (str): + course_id (str): + role (str): + """ + + org: str + course_id: str + role: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + org = self.org + + course_id = self.course_id + + role = self.role + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "org": org, + "course_id": course_id, + "role": role, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + org = d.pop("org") + + course_id = d.pop("course_id") + + role = d.pop("role") + + user_role = cls( + org=org, + course_id=course_id, + role=role, + ) + + user_role.additional_properties = d + return user_role + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/user_roles_response.py b/openedx_platform_sdk/models/user_roles_response.py new file mode 100644 index 0000000..4092d81 --- /dev/null +++ b/openedx_platform_sdk/models/user_roles_response.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.user_role import UserRole + + +T = TypeVar("T", bound="UserRolesResponse") + + +@_attrs_define +class UserRolesResponse: + """Serializes the full response payload for UserRolesViewSet (ADR 0025). + + Attributes: + roles (list[UserRole]): + is_staff (bool): + """ + + roles: list[UserRole] + is_staff: bool + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + roles = [] + for roles_item_data in self.roles: + roles_item = roles_item_data.to_dict() + roles.append(roles_item) + + is_staff = self.is_staff + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "roles": roles, + "is_staff": is_staff, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.user_role import UserRole + + d = dict(src_dict) + roles = [] + _roles = d.pop("roles") + for roles_item_data in _roles: + roles_item = UserRole.from_dict(roles_item_data) + + roles.append(roles_item) + + is_staff = d.pop("is_staff") + + user_roles_response = cls( + roles=roles, + is_staff=is_staff, + ) + + user_roles_response.additional_properties = d + return user_roles_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/v2_enrollment_list_view.py b/openedx_platform_sdk/models/v2_enrollment_list_view.py new file mode 100644 index 0000000..afc41b1 --- /dev/null +++ b/openedx_platform_sdk/models/v2_enrollment_list_view.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class V2EnrollmentListView(str, Enum): + MINIMAL = "minimal" + + def __str__(self) -> str: + return str(self.value) diff --git a/openedx_platform_sdk/models/v2_enrollment_unenroll_create_data_body.py b/openedx_platform_sdk/models/v2_enrollment_unenroll_create_data_body.py new file mode 100644 index 0000000..85657bf --- /dev/null +++ b/openedx_platform_sdk/models/v2_enrollment_unenroll_create_data_body.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="V2EnrollmentUnenrollCreateDataBody") + + +@_attrs_define +class V2EnrollmentUnenrollCreateDataBody: + """ + Attributes: + username (str): + """ + + username: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + username = self.username + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "username": username, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + username = d.pop("username") + + v2_enrollment_unenroll_create_data_body = cls( + username=username, + ) + + v2_enrollment_unenroll_create_data_body.additional_properties = d + return v2_enrollment_unenroll_create_data_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/v2_enrollment_unenroll_create_files_body.py b/openedx_platform_sdk/models/v2_enrollment_unenroll_create_files_body.py new file mode 100644 index 0000000..4049b59 --- /dev/null +++ b/openedx_platform_sdk/models/v2_enrollment_unenroll_create_files_body.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types + +T = TypeVar("T", bound="V2EnrollmentUnenrollCreateFilesBody") + + +@_attrs_define +class V2EnrollmentUnenrollCreateFilesBody: + """ + Attributes: + username (str): + """ + + username: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + username = self.username + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "username": username, + } + ) + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + files.append(("username", (None, str(self.username).encode(), "text/plain"))) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + username = d.pop("username") + + v2_enrollment_unenroll_create_files_body = cls( + username=username, + ) + + v2_enrollment_unenroll_create_files_body.additional_properties = d + return v2_enrollment_unenroll_create_files_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openedx_platform_sdk/models/v2_enrollment_unenroll_create_json_body.py b/openedx_platform_sdk/models/v2_enrollment_unenroll_create_json_body.py new file mode 100644 index 0000000..6c1d0ee --- /dev/null +++ b/openedx_platform_sdk/models/v2_enrollment_unenroll_create_json_body.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="V2EnrollmentUnenrollCreateJsonBody") + + +@_attrs_define +class V2EnrollmentUnenrollCreateJsonBody: + """ + Attributes: + username (str): + """ + + username: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + username = self.username + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "username": username, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + username = d.pop("username") + + v2_enrollment_unenroll_create_json_body = cls( + username=username, + ) + + v2_enrollment_unenroll_create_json_body.additional_properties = d + return v2_enrollment_unenroll_create_json_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/regen_sdk.sh b/regen_sdk.sh index 505409c..f04851b 100755 --- a/regen_sdk.sh +++ b/regen_sdk.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash -# Regenerate openedx-platform-sdk from the openedx-platform OpenAPI schema. +# Regenerate openedx-platform-sdk from Studio (CMS) and LMS OpenAPI schemas. # # Usage: -# ./regen_sdk.sh # regenerate from running Studio (no platform repo needed) +# ./regen_sdk.sh # regenerate from running devstack # ./regen_sdk.sh feat/axim-api_improvements # checkout branch first, then regenerate # # Environment variables: # STUDIO_URL Base URL of running Studio (default: http://studio.local.openedx.io:8001) +# LMS_URL Base URL of running LMS (default: http://local.openedx.io:8000) # PLATFORM_DIR Path to local openedx-platform repo (default: ../openedx-platform) # Only needed when passing a branch argument. # @@ -17,10 +18,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCHEMA_FILE="$SCRIPT_DIR/schema.yml" +LMS_SCHEMA_FILE="$SCRIPT_DIR/lms_schema.yml" FILTERED_SCHEMA_FILE="$SCRIPT_DIR/filtered_schema.yml" FILTER_SCRIPT="$SCRIPT_DIR/filter_schema.py" CONFIG_FILE="$SCRIPT_DIR/config.yml" STUDIO_URL="${STUDIO_URL:-http://studio.local.openedx.io:8001}" +LMS_URL="${LMS_URL:-http://local.openedx.io:8000}" PLATFORM_DIR="${PLATFORM_DIR:-$(dirname "$SCRIPT_DIR")/openedx-platform}" SDK_TAG="openedx-platform-sdk" @@ -39,19 +42,26 @@ if [[ -n "$BRANCH" ]]; then echo " Current branch: $(git -C "$PLATFORM_DIR" branch --show-current)" fi -# ── 2. Download schema from running Studio ──────────────────────────────────── -echo "→ Downloading OpenAPI schema from $STUDIO_URL ..." +# ── 2. Download schemas from Studio and LMS ─────────────────────────────────── +echo "→ Downloading Studio schema from $STUDIO_URL ..." if ! curl -sf "$STUDIO_URL/authoring-api/schema/" -o "$SCHEMA_FILE"; then echo "Error: Could not reach $STUDIO_URL/authoring-api/schema/" - echo "Make sure Studio is running. Override the URL with:" - echo " STUDIO_URL=http://your-studio-url:port ./regen_sdk.sh" + echo "Make sure Studio is running. Override with: STUDIO_URL=http://... ./regen_sdk.sh" exit 1 fi -echo " Schema written to: $SCHEMA_FILE" +echo " Studio schema written to: $SCHEMA_FILE" -# ── 3. Filter schema to SDK-tagged paths only ───────────────────────────────── -echo "→ Filtering schema to tag '$SDK_TAG'..." -python "$FILTER_SCRIPT" "$SCHEMA_FILE" "$FILTERED_SCHEMA_FILE" "$SDK_TAG" +echo "→ Downloading LMS schema from $LMS_URL ..." +if ! curl -sf "$LMS_URL/lms-api/schema/" -o "$LMS_SCHEMA_FILE"; then + echo "Error: Could not reach $LMS_URL/lms-api/schema/" + echo "Make sure LMS is running. Override with: LMS_URL=http://... ./regen_sdk.sh" + exit 1 +fi +echo " LMS schema written to: $LMS_SCHEMA_FILE" + +# ── 3. Filter and merge both schemas ───────────────────────────────────────── +echo "→ Filtering and merging schemas for tag '$SDK_TAG'..." +python "$FILTER_SCRIPT" "$SCHEMA_FILE" "$FILTERED_SCHEMA_FILE" "$SDK_TAG" --merge "$LMS_SCHEMA_FILE" # ── 4. Regenerate the SDK ───────────────────────────────────────────────────── echo "→ Regenerating SDK..." @@ -86,11 +96,12 @@ fi # Fix generator bugs in API modules that have nested-object bodies. API_DIR="$SCRIPT_DIR/openedx_platform_sdk/api/openedx_platform_sdk" -# Bug 1: v3_course_details_update uses Unset in type annotations but imports only UNSET. -UPDATE_FILE="$API_DIR/v3_course_details_update.py" -if [[ -f "$UPDATE_FILE" ]]; then - sed -i '' 's/from \.\.\.types import UNSET, Response$/from ...types import UNSET, Response, Unset/' "$UPDATE_FILE" -fi +# Bug 1: Some generated files use Unset in type annotations but import only UNSET. +# Apply the fix to every API file that has this pattern. +grep -rl 'from \.\.\.types import UNSET, Response$' "$API_DIR" | while read -r f; do + sed -i '' 's/from \.\.\.types import UNSET, Response$/from ...types import UNSET, Response, Unset/' "$f" + echo " Fixed missing Unset import in $(basename "$f")" +done # Bug 2: _get_kwargs has three identical isinstance(body, X) blocks for json/form/multipart. # The multipart block always wins and breaks nested-dict payloads. Keep only JSON block. @@ -178,6 +189,83 @@ for path in glob.glob(os.path.join(models_dir, "*.py")): print(f" Fixed dict-safe to_dict() in {os.path.basename(path)}") PYEOF +# Bug 4: EnrollmentCourse datetime fields (enrollment_start, enrollment_end, course_start, +# course_end) are marked required in the schema but the API returns null for courses with no +# dates set. Fix: replace datetime.datetime.fromisoformat(d.pop(field)) calls with a +# null-safe helper inside from_dict() for affected models. +python3 - "$MODELS_DIR" <<'PYEOF' +import sys, os, glob, re + +models_dir = sys.argv[1] +# Pattern: bare `datetime.datetime.fromisoformat(d.pop("field"))` inside from_dict +DT_PATTERN = re.compile( + r'^(\s+)(\w+) = datetime\.datetime\.fromisoformat\(d\.pop\("(\w+)"\)\)\s*$' +) + +for path in glob.glob(os.path.join(models_dir, "*.py")): + lines = open(path).readlines() + new_lines = [] + changed = False + in_from_dict = False + + for line in lines: + if "def from_dict(" in line: + in_from_dict = True + elif in_from_dict and re.match(r'^ def ', line) and "from_dict" not in line: + in_from_dict = False + + if in_from_dict: + m = DT_PATTERN.match(line) + if m: + indent, var, key = m.group(1), m.group(2), m.group(3) + # Emit a null-safe helper on first match in this file + if not changed: + # Insert helper before first occurrence — handled inline below + pass + new_lines.append( + f"{indent}_raw_{var} = d.pop(\"{key}\")\n" + f"{indent}{var} = datetime.datetime.fromisoformat(_raw_{var}) " + f"if isinstance(_raw_{var}, str) else None\n" + ) + changed = True + continue + new_lines.append(line) + + if changed: + open(path, 'w').writelines(new_lines) + print(f" Fixed null-safe datetime parsing in {os.path.basename(path)}") +PYEOF + +# Bug 5: The GET /v2/enrollment/enrollment_allowed/ endpoint returns a plain JSON array +# instead of the expected paginated envelope {"count":N,"results":[...]}. +# Fix: add an isinstance(src_dict, list) guard in from_dict() so a bare list is +# normalised into the envelope shape before the regular parsing runs. +python3 - "$MODELS_DIR" <<'PYEOF' +import sys, os, glob + +models_dir = sys.argv[1] +TARGET = "paginated_course_enrollment_allowed_list.py" +# Include the leading 8 spaces so str.replace matches the full line, not just a substring. +MARKER = " d = dict(src_dict)" +INSERT = ( + " if isinstance(src_dict, list):\n" + " src_dict = {\"count\": len(src_dict), \"results\": src_dict}\n" +) + +for path in glob.glob(os.path.join(models_dir, "*.py")): + if os.path.basename(path) != TARGET: + continue + text = open(path).read() + if INSERT in text: + break # already patched + if MARKER not in text: + print(f" WARNING: could not find marker in {TARGET}; skipping Bug 5 fix") + break + text = text.replace(MARKER, INSERT + MARKER, 1) + open(path, 'w').write(text) + print(f" Fixed plain-list response in {TARGET}") +PYEOF + echo "" echo "✓ SDK regenerated successfully." echo " Package : openedx_platform_sdk"