e2x-course-hub 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. e2x_course_hub-0.2.0/.github/workflows/ci.yml +97 -0
  2. e2x_course_hub-0.2.0/.github/workflows/python-publish.yaml +53 -0
  3. e2x_course_hub-0.2.0/.gitignore +143 -0
  4. e2x_course_hub-0.2.0/LICENSE +21 -0
  5. e2x_course_hub-0.2.0/PKG-INFO +269 -0
  6. e2x_course_hub-0.2.0/README.md +228 -0
  7. e2x_course_hub-0.2.0/course-service-ui/.env.development +4 -0
  8. e2x_course_hub-0.2.0/course-service-ui/.gitignore +24 -0
  9. e2x_course_hub-0.2.0/course-service-ui/.prettierignore +3 -0
  10. e2x_course_hub-0.2.0/course-service-ui/eslint.config.js +23 -0
  11. e2x_course_hub-0.2.0/course-service-ui/package-lock.json +4937 -0
  12. e2x_course_hub-0.2.0/course-service-ui/package.json +53 -0
  13. e2x_course_hub-0.2.0/course-service-ui/public/favicon.ico +0 -0
  14. e2x_course_hub-0.2.0/course-service-ui/src/App.tsx +21 -0
  15. e2x_course_hub-0.2.0/course-service-ui/src/api/client.ts +130 -0
  16. e2x_course_hub-0.2.0/course-service-ui/src/api/courses.ts +105 -0
  17. e2x_course_hub-0.2.0/course-service-ui/src/api/http.ts +25 -0
  18. e2x_course_hub-0.2.0/course-service-ui/src/api/index.ts +12 -0
  19. e2x_course_hub-0.2.0/course-service-ui/src/api/infrastructure.ts +29 -0
  20. e2x_course_hub-0.2.0/course-service-ui/src/api/me.ts +12 -0
  21. e2x_course_hub-0.2.0/course-service-ui/src/api/membership.ts +118 -0
  22. e2x_course_hub-0.2.0/course-service-ui/src/api/roles.ts +14 -0
  23. e2x_course_hub-0.2.0/course-service-ui/src/api/schema.d.ts +1789 -0
  24. e2x_course_hub-0.2.0/course-service-ui/src/api/types.ts +147 -0
  25. e2x_course_hub-0.2.0/course-service-ui/src/components/actions/ActionList.tsx +162 -0
  26. e2x_course_hub-0.2.0/course-service-ui/src/components/actions/RoleActionsModal.tsx +190 -0
  27. e2x_course_hub-0.2.0/course-service-ui/src/components/actions/RoleInfoLink.tsx +34 -0
  28. e2x_course_hub-0.2.0/course-service-ui/src/components/actions/YourAccessButton.tsx +46 -0
  29. e2x_course_hub-0.2.0/course-service-ui/src/components/course/CoursePage.tsx +81 -0
  30. e2x_course_hub-0.2.0/course-service-ui/src/components/course/tabs/CourseOverviewTab.tsx +165 -0
  31. e2x_course_hub-0.2.0/course-service-ui/src/components/course/tabs/CourseOwnersTab.tsx +34 -0
  32. e2x_course_hub-0.2.0/course-service-ui/src/components/course/tabs/CourseSettingsTab.tsx +179 -0
  33. e2x_course_hub-0.2.0/course-service-ui/src/components/courses/CoursesPage.tsx +61 -0
  34. e2x_course_hub-0.2.0/course-service-ui/src/components/courses/tabs/CourseCreatorsTab.tsx +29 -0
  35. e2x_course_hub-0.2.0/course-service-ui/src/components/courses/tabs/CourseListTab.tsx +140 -0
  36. e2x_course_hub-0.2.0/course-service-ui/src/components/courses/tabs/LMSAdminsTab.tsx +29 -0
  37. e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/CreateCourseDialog.tsx +137 -0
  38. e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/CreateTermDialog.tsx +95 -0
  39. e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/ImageSelectionDialog.tsx +113 -0
  40. e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/ResourceSelectionDialog.tsx +93 -0
  41. e2x_course_hub-0.2.0/course-service-ui/src/components/infrastructure/RuntimeConfigEditor.tsx +156 -0
  42. e2x_course_hub-0.2.0/course-service-ui/src/components/membership/AddMembersDialog.tsx +93 -0
  43. e2x_course_hub-0.2.0/course-service-ui/src/components/membership/MemberDataTable.tsx +185 -0
  44. e2x_course_hub-0.2.0/course-service-ui/src/components/membership/MemberDataTableWithCurrentUser.tsx +12 -0
  45. e2x_course_hub-0.2.0/course-service-ui/src/components/membership/MemberListCard.tsx +118 -0
  46. e2x_course_hub-0.2.0/course-service-ui/src/components/membership/TabbedMemberDataTable.tsx +107 -0
  47. e2x_course_hub-0.2.0/course-service-ui/src/components/membership/TabbedMemberDataTableWithCurrentUser.tsx +11 -0
  48. e2x_course_hub-0.2.0/course-service-ui/src/components/term/TermPage.tsx +81 -0
  49. e2x_course_hub-0.2.0/course-service-ui/src/components/term/tabs/TermOverviewTab.tsx +252 -0
  50. e2x_course_hub-0.2.0/course-service-ui/src/components/term/tabs/TermSettingsTab.tsx +95 -0
  51. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Alert.tsx +35 -0
  52. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Badge.tsx +20 -0
  53. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Button.tsx +30 -0
  54. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Card.tsx +24 -0
  55. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/ConfirmDialog.tsx +132 -0
  56. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/DataTable.tsx +322 -0
  57. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Modal.tsx +71 -0
  58. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Row.tsx +19 -0
  59. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/SelectionBox.tsx +48 -0
  60. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Switch.tsx +33 -0
  61. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/TabBar.tsx +33 -0
  62. e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Tabs.tsx +52 -0
  63. e2x_course_hub-0.2.0/course-service-ui/src/config.ts +27 -0
  64. e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/courses.ts +97 -0
  65. e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/index.ts +5 -0
  66. e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/membership.ts +42 -0
  67. e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/terms.ts +47 -0
  68. e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/tree.ts +6 -0
  69. e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/types.ts +17 -0
  70. e2x_course_hub-0.2.0/course-service-ui/src/domain/roles.ts +39 -0
  71. e2x_course_hub-0.2.0/course-service-ui/src/hooks/catalog/index.ts +2 -0
  72. e2x_course_hub-0.2.0/course-service-ui/src/hooks/catalog/keys.ts +6 -0
  73. e2x_course_hub-0.2.0/course-service-ui/src/hooks/catalog/queries.ts +29 -0
  74. e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/index.ts +3 -0
  75. e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/keys.ts +12 -0
  76. e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/mutations.ts +130 -0
  77. e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/queries.ts +67 -0
  78. e2x_course_hub-0.2.0/course-service-ui/src/hooks/index.ts +0 -0
  79. e2x_course_hub-0.2.0/course-service-ui/src/hooks/me/index.ts +1 -0
  80. e2x_course_hub-0.2.0/course-service-ui/src/hooks/me/queries.ts +10 -0
  81. e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/index.ts +3 -0
  82. e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/keys.ts +54 -0
  83. e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/mutations.ts +161 -0
  84. e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/queries.ts +111 -0
  85. e2x_course_hub-0.2.0/course-service-ui/src/hooks/roles/index.ts +1 -0
  86. e2x_course_hub-0.2.0/course-service-ui/src/hooks/roles/queries.ts +16 -0
  87. e2x_course_hub-0.2.0/course-service-ui/src/hooks/ui/index.ts +2 -0
  88. e2x_course_hub-0.2.0/course-service-ui/src/hooks/ui/useBodyScrollLock.ts +17 -0
  89. e2x_course_hub-0.2.0/course-service-ui/src/hooks/ui/useSelection.ts +56 -0
  90. e2x_course_hub-0.2.0/course-service-ui/src/index.css +34 -0
  91. e2x_course_hub-0.2.0/course-service-ui/src/lib/buildDefaultCourseConfig.ts +59 -0
  92. e2x_course_hub-0.2.0/course-service-ui/src/lib/dataTablePageSize.ts +15 -0
  93. e2x_course_hub-0.2.0/course-service-ui/src/lib/errorMessage.ts +28 -0
  94. e2x_course_hub-0.2.0/course-service-ui/src/main.tsx +18 -0
  95. e2x_course_hub-0.2.0/course-service-ui/src/queryClient.ts +3 -0
  96. e2x_course_hub-0.2.0/course-service-ui/tsconfig.app.json +39 -0
  97. e2x_course_hub-0.2.0/course-service-ui/tsconfig.json +18 -0
  98. e2x_course_hub-0.2.0/course-service-ui/tsconfig.node.json +26 -0
  99. e2x_course_hub-0.2.0/course-service-ui/vite.config.ts +42 -0
  100. e2x_course_hub-0.2.0/docs/Makefile +21 -0
  101. e2x_course_hub-0.2.0/docs/make.bat +35 -0
  102. e2x_course_hub-0.2.0/docs/requirements.txt +6 -0
  103. e2x_course_hub-0.2.0/docs/source/conf.py +53 -0
  104. e2x_course_hub-0.2.0/docs/source/course_service/overview.md +85 -0
  105. e2x_course_hub-0.2.0/docs/source/getting_started/installation.md +24 -0
  106. e2x_course_hub-0.2.0/docs/source/images/course_overview.png +0 -0
  107. e2x_course_hub-0.2.0/docs/source/images/course_select_notebook_image.png +0 -0
  108. e2x_course_hub-0.2.0/docs/source/images/course_select_student_resources.png +0 -0
  109. e2x_course_hub-0.2.0/docs/source/images/course_service_courses.png +0 -0
  110. e2x_course_hub-0.2.0/docs/source/images/term_add_students.png +0 -0
  111. e2x_course_hub-0.2.0/docs/source/images/term_overview.png +0 -0
  112. e2x_course_hub-0.2.0/docs/source/images/term_what_can_a_student_do.png +0 -0
  113. e2x_course_hub-0.2.0/docs/source/index.md +62 -0
  114. e2x_course_hub-0.2.0/docs/source/reference/configuration.md +52 -0
  115. e2x_course_hub-0.2.0/docs/source/reference/rest_api.md +45 -0
  116. e2x_course_hub-0.2.0/docs/source/reference/roles.md +86 -0
  117. e2x_course_hub-0.2.0/docs/source/spawner/contract.md +39 -0
  118. e2x_course_hub-0.2.0/docs/source/user_guide/courses.md +85 -0
  119. e2x_course_hub-0.2.0/docs/source/user_guide/members.md +51 -0
  120. e2x_course_hub-0.2.0/docs/source/user_guide/semesters.md +56 -0
  121. e2x_course_hub-0.2.0/e2x_course_hub/__about__.py +1 -0
  122. e2x_course_hub-0.2.0/e2x_course_hub/__init__.py +0 -0
  123. e2x_course_hub-0.2.0/e2x_course_hub/api/__init__.py +6 -0
  124. e2x_course_hub-0.2.0/e2x_course_hub/api/api.py +42 -0
  125. e2x_course_hub-0.2.0/e2x_course_hub/api/course_api.py +536 -0
  126. e2x_course_hub-0.2.0/e2x_course_hub/api/course_permissions.py +101 -0
  127. e2x_course_hub-0.2.0/e2x_course_hub/api/infrastructure_api.py +43 -0
  128. e2x_course_hub-0.2.0/e2x_course_hub/api/infrastructure_permissions.py +51 -0
  129. e2x_course_hub-0.2.0/e2x_course_hub/api/membership_api.py +47 -0
  130. e2x_course_hub-0.2.0/e2x_course_hub/api/role_permissions.py +27 -0
  131. e2x_course_hub-0.2.0/e2x_course_hub/api/spawn_offering_api.py +60 -0
  132. e2x_course_hub-0.2.0/e2x_course_hub/api/spawn_permissions.py +70 -0
  133. e2x_course_hub-0.2.0/e2x_course_hub/contract/README.md +143 -0
  134. e2x_course_hub-0.2.0/e2x_course_hub/contract/__init__.py +46 -0
  135. e2x_course_hub-0.2.0/e2x_course_hub/contract/providers.py +27 -0
  136. e2x_course_hub-0.2.0/e2x_course_hub/course_service/__init__.py +0 -0
  137. e2x_course_hub-0.2.0/e2x_course_hub/course_service/_data.py +25 -0
  138. e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/__init__.py +23 -0
  139. e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/builders.py +149 -0
  140. e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/can.py +34 -0
  141. e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/models.py +93 -0
  142. e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/__init__.py +0 -0
  143. e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/dependencies.py +56 -0
  144. e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/dependency_types.py +24 -0
  145. e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/schemas.py +20 -0
  146. e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/__init__.py +0 -0
  147. e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/assembler.py +102 -0
  148. e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/dependencies.py +45 -0
  149. e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/router.py +131 -0
  150. e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/schemas.py +30 -0
  151. e2x_course_hub-0.2.0/e2x_course_hub/course_service/exception_handlers.py +21 -0
  152. e2x_course_hub-0.2.0/e2x_course_hub/course_service/fastapi_app.py +100 -0
  153. e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/__init__.py +0 -0
  154. e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/assembler.py +37 -0
  155. e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/dependencies.py +17 -0
  156. e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/router.py +35 -0
  157. e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/schemas.py +20 -0
  158. e2x_course_hub-0.2.0/e2x_course_hub/course_service/me/__init__.py +0 -0
  159. e2x_course_hub-0.2.0/e2x_course_hub/course_service/me/router.py +22 -0
  160. e2x_course_hub-0.2.0/e2x_course_hub/course_service/me/schemas.py +5 -0
  161. e2x_course_hub-0.2.0/e2x_course_hub/course_service/membership/__init__.py +0 -0
  162. e2x_course_hub-0.2.0/e2x_course_hub/course_service/membership/router.py +184 -0
  163. e2x_course_hub-0.2.0/e2x_course_hub/course_service/membership/schemas.py +15 -0
  164. e2x_course_hub-0.2.0/e2x_course_hub/course_service/roles/__init__.py +0 -0
  165. e2x_course_hub-0.2.0/e2x_course_hub/course_service/roles/router.py +26 -0
  166. e2x_course_hub-0.2.0/e2x_course_hub/course_service/roles/schemas.py +24 -0
  167. e2x_course_hub-0.2.0/e2x_course_hub/course_service/settings.py +11 -0
  168. e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/__init__.py +0 -0
  169. e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/assembler.py +67 -0
  170. e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/dependencies.py +38 -0
  171. e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/router.py +83 -0
  172. e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/schemas.py +24 -0
  173. e2x_course_hub-0.2.0/e2x_course_hub/course_service/whoami.py +40 -0
  174. e2x_course_hub-0.2.0/e2x_course_hub/db/__init__.py +7 -0
  175. e2x_course_hub-0.2.0/e2x_course_hub/db/database.py +53 -0
  176. e2x_course_hub-0.2.0/e2x_course_hub/db/models.py +106 -0
  177. e2x_course_hub-0.2.0/e2x_course_hub/db/repository.py +423 -0
  178. e2x_course_hub-0.2.0/e2x_course_hub/errors.py +187 -0
  179. e2x_course_hub-0.2.0/e2x_course_hub/loader.py +110 -0
  180. e2x_course_hub-0.2.0/e2x_course_hub/schema/catalog.py +211 -0
  181. e2x_course_hub-0.2.0/e2x_course_hub/schema/course.py +58 -0
  182. e2x_course_hub-0.2.0/e2x_course_hub/schema/selection.py +80 -0
  183. e2x_course_hub-0.2.0/e2x_course_hub/schema/types.py +20 -0
  184. e2x_course_hub-0.2.0/e2x_course_hub/settings.py +37 -0
  185. e2x_course_hub-0.2.0/e2x_course_hub/utils.py +64 -0
  186. e2x_course_hub-0.2.0/openapi.json +2470 -0
  187. e2x_course_hub-0.2.0/pyproject.toml +124 -0
  188. e2x_course_hub-0.2.0/scripts/export_openapi.py +70 -0
  189. e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/assets/main.css +1 -0
  190. e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/favicon.ico +0 -0
  191. e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/main.js +20 -0
  192. e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/main.js.map +1 -0
  193. e2x_course_hub-0.2.0/share/e2x_course_hub/templates/course_service/react_base.j2 +21 -0
@@ -0,0 +1,97 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ concurrency:
8
+ group: ${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ python:
13
+ name: Python lint
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+
18
+ # Ruff reads its config from pyproject.toml and never imports the package,
19
+ # so this job deliberately skips installing the project — doing so would
20
+ # drag in Node just to build the UI bundle (see the frontend job).
21
+ #
22
+ # The action installs the ruff version pinned in pyproject.toml's `dev`
23
+ # extra. That pin matters: formatter output changes between releases, so
24
+ # an unpinned ruff makes CI disagree with developers' local ruff without
25
+ # anything in the repository having changed.
26
+ - name: Check formatting
27
+ uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
28
+ with:
29
+ args: "format --check --diff"
30
+
31
+ - name: Lint
32
+ if: ${{ !cancelled() }}
33
+ uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
34
+ with:
35
+ args: "check --output-format=github"
36
+
37
+ frontend:
38
+ name: Frontend and API contract (Python ${{ matrix.python-version }})
39
+ runs-on: ubuntu-latest
40
+ # Installing the package and exporting the OpenAPI schema is the only
41
+ # exercise the Python code gets, so run it on every version pyproject.toml
42
+ # claims to support. The UI-only steps below run once, on the newest.
43
+ strategy:
44
+ fail-fast: false
45
+ matrix:
46
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
47
+ steps:
48
+ - uses: actions/checkout@v7
49
+
50
+ # Node must be set up before the Python install: the hatch-jupyter-builder
51
+ # hook in pyproject.toml shells out to npm, so `pip install .` aborts with
52
+ # "Could not find cmd (npm) in path" when Node is missing — even while
53
+ # only generating package metadata.
54
+ - uses: actions/setup-node@v7
55
+ with:
56
+ node-version: "24"
57
+ cache: npm
58
+ cache-dependency-path: course-service-ui/package-lock.json
59
+
60
+ - uses: actions/setup-python@v7
61
+ with:
62
+ python-version: ${{ matrix.python-version }}
63
+
64
+ - name: Install UI dependencies
65
+ run: npm ci
66
+ working-directory: course-service-ui
67
+
68
+ # Installing the project makes e2x_course_hub importable for the OpenAPI
69
+ # export below, and exercises the packaging path as a side effect: the
70
+ # build hook bundles the UI exactly as a released wheel would.
71
+ - name: Install the course hub
72
+ run: pip install .
73
+
74
+ # Regenerates openapi.json and schema.d.ts and fails if either differs
75
+ # from what is committed, so a backend schema change cannot land without
76
+ # the matching client types.
77
+ - name: Check the generated API types are up to date
78
+ run: npm run check:api-types
79
+ working-directory: course-service-ui
80
+ env:
81
+ PYTHON: python
82
+
83
+ - name: Lint
84
+ if: matrix.python-version == '3.14'
85
+ run: npm run lint
86
+ working-directory: course-service-ui
87
+
88
+ - name: Check formatting
89
+ if: matrix.python-version == '3.14'
90
+ run: npx prettier --check src
91
+ working-directory: course-service-ui
92
+
93
+ # Runs `tsc -b` and then the production bundle.
94
+ - name: Typecheck and build
95
+ if: matrix.python-version == '3.14'
96
+ run: npm run build
97
+ working-directory: course-service-ui
@@ -0,0 +1,53 @@
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ release-build:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+
17
+ - uses: actions/setup-python@v7
18
+ with:
19
+ python-version: "3.x"
20
+
21
+ - name: Build release distributions
22
+ run: |
23
+ # NOTE: put your own distribution build steps here.
24
+ python -m pip install build
25
+ python -m build
26
+
27
+ - name: Upload distributions
28
+ uses: actions/upload-artifact@v7
29
+ with:
30
+ name: release-dists
31
+ path: dist/
32
+
33
+ pypi-publish:
34
+ runs-on: ubuntu-latest
35
+ needs:
36
+ - release-build
37
+ permissions:
38
+ id-token: write
39
+ environment:
40
+ name: pypi
41
+
42
+
43
+ steps:
44
+ - name: Retrieve release distributions
45
+ uses: actions/download-artifact@v8
46
+ with:
47
+ name: release-dists
48
+ path: dist/
49
+
50
+ - name: Publish release distributions to PyPI
51
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
52
+ with:
53
+ packages-dir: dist/
@@ -0,0 +1,143 @@
1
+ # Node modules
2
+
3
+ node_modules
4
+
5
+ # Meta files
6
+ ui/meta.json
7
+
8
+ # Build static files
9
+ share/e2x_course_hub/static/
10
+
11
+ # Byte-compiled / optimized / DLL files
12
+ __pycache__/
13
+ *.py[cod]
14
+ *$py.class
15
+
16
+ # C extensions
17
+ *.so
18
+
19
+ # Distribution / packaging
20
+ .Python
21
+ build/
22
+ develop-eggs/
23
+ dist/
24
+ downloads/
25
+ eggs/
26
+ .eggs/
27
+ lib/*
28
+ !src/lib/
29
+ lib64/
30
+ parts/
31
+ sdist/
32
+ var/
33
+ wheels/
34
+ pip-wheel-metadata/
35
+ share/python-wheels/
36
+ *.egg-info/
37
+ .installed.cfg
38
+ *.egg
39
+ MANIFEST
40
+
41
+ # PyInstaller
42
+ # Usually these files are written by a python script from a template
43
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
44
+ *.manifest
45
+ *.spec
46
+
47
+ # Installer logs
48
+ pip-log.txt
49
+ pip-delete-this-directory.txt
50
+
51
+ # Unit test / coverage reports
52
+ htmlcov/
53
+ .tox/
54
+ .nox/
55
+ .coverage
56
+ .coverage.*
57
+ .cache
58
+ nosetests.xml
59
+ coverage.xml
60
+ *.cover
61
+ *.py,cover
62
+ .hypothesis/
63
+ .pytest_cache/
64
+
65
+ # Translations
66
+ *.mo
67
+ *.pot
68
+
69
+ # Django stuff:
70
+ *.log
71
+ local_settings.py
72
+ db.sqlite3
73
+ db.sqlite3-journal
74
+
75
+ # Flask stuff:
76
+ instance/
77
+ .webassets-cache
78
+
79
+ # Scrapy stuff:
80
+ .scrapy
81
+
82
+ # Sphinx documentation
83
+ docs/_build/
84
+
85
+ # PyBuilder
86
+ target/
87
+
88
+ # Jupyter Notebook
89
+ .ipynb_checkpoints
90
+
91
+ # IPython
92
+ profile_default/
93
+ ipython_config.py
94
+
95
+ # pyenv
96
+ .python-version
97
+
98
+ # pipenv
99
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
101
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
102
+ # install all needed dependencies.
103
+ #Pipfile.lock
104
+
105
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
106
+ __pypackages__/
107
+
108
+ # Celery stuff
109
+ celerybeat-schedule
110
+ celerybeat.pid
111
+
112
+ # SageMath parsed files
113
+ *.sage.py
114
+
115
+ # Environments
116
+ .env
117
+ .venv
118
+ env/
119
+ venv/
120
+ ENV/
121
+ env.bak/
122
+ venv.bak/
123
+
124
+ # Spyder project settings
125
+ .spyderproject
126
+ .spyproject
127
+
128
+ # Rope project settings
129
+ .ropeproject
130
+
131
+ # mkdocs documentation
132
+ /site
133
+
134
+ # mypy
135
+ .mypy_cache/
136
+ .dmypy.json
137
+ dmypy.json
138
+
139
+ # Pyre type checker
140
+ .pyre/
141
+
142
+ # vscode
143
+ .vscode/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 DigiKlausur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,269 @@
1
+ Metadata-Version: 2.5
2
+ Name: e2x-course-hub
3
+ Version: 0.2.0
4
+ Summary: A JupyterHub service for managing courses, terms, and course membership with role-based access control in multi-course teaching deployments.
5
+ Project-URL: Documentation, https://github.com/Digiklausur/e2x-course-hub#readme
6
+ Project-URL: Issues, https://github.com/Digiklausur/e2x-course-hub/issues
7
+ Project-URL: Source, https://github.com/Digiklausur/e2x-course-hub
8
+ Author-email: Tim Metzler <tim.metzler@h-brs.de>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: e2x-hub-rbac>=0.3.0
19
+ Requires-Dist: fastapi>=0.100.0
20
+ Requires-Dist: httpx>=0.24
21
+ Requires-Dist: jinja2>=3.1
22
+ Requires-Dist: jupyterhub-fastapi-adapter>=0.1.0
23
+ Requires-Dist: pydantic-settings>=2.0
24
+ Requires-Dist: pydantic>=2.0
25
+ Requires-Dist: python-multipart>=0.0.6
26
+ Requires-Dist: pyyaml>=6.0
27
+ Requires-Dist: sqlalchemy>=2.0
28
+ Requires-Dist: uvicorn>=0.23
29
+ Provides-Extra: dev
30
+ Requires-Dist: build; extra == 'dev'
31
+ Requires-Dist: pre-commit; extra == 'dev'
32
+ Requires-Dist: ruff==0.16.7; extra == 'dev'
33
+ Requires-Dist: tbump; extra == 'dev'
34
+ Requires-Dist: twine; extra == 'dev'
35
+ Provides-Extra: docs
36
+ Requires-Dist: autodoc-pydantic>=2.0; extra == 'docs'
37
+ Requires-Dist: myst-parser; extra == 'docs'
38
+ Requires-Dist: pydata-sphinx-theme; extra == 'docs'
39
+ Requires-Dist: sphinx; extra == 'docs'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # e2x Course Hub
43
+
44
+ A JupyterHub service for running multi-course, multi-term teaching deployments. It manages courses, terms, and course membership; enforces who may do what
45
+ via role-based access control; and tells a separate infrastructure spawner which
46
+ courses/terms a user may launch and with which environment — without ever deciding
47
+ hardware itself.
48
+
49
+ It ships as a FastAPI backend (REST API under `/v1/...`) plus a React single-page app
50
+ (`course-service-ui`) for course and membership management, both served from the same
51
+ process.
52
+
53
+ ## What it does
54
+
55
+ - **Course & term management** — courses and their terms are stored in a database
56
+ (SQLAlchemy) and managed through a REST API: create/remove courses and terms, edit
57
+ metadata, and configure the environment (image, resource tier, profile) per course,
58
+ overridable per term.
59
+ - **Role-based access control** — powered by [`e2x-hub-rbac`](https://github.com/Digiklausur/e2x-hub-rbac).
60
+ Roles (LMS admin, course creator, course owner, instructor, teaching assistant,
61
+ student, observer) are scoped to the LMS, a course, or a term, and membership is
62
+ backed by JupyterHub groups.
63
+ - **A typed contract with the spawner** — `e2x_course_hub.contract` defines the
64
+ boundary between this package and the JupyterHub Spawner that actually launches
65
+ containers. The course hub owns authorization and course configuration; the spawner
66
+ owns the runtime. They meet only through two `Protocol`s:
67
+ - `SpawnOfferingProvider` (implemented here): tells the spawner, for a given user,
68
+ which course/terms they may launch, in which role, with which selection.
69
+ - `InfrastructureCatalogProvider` (implemented by the spawner): tells the course hub
70
+ what images, resource tiers, and profiles actually exist, so course configuration
71
+ can only ever offer valid choices.
72
+ - **A management UI** — `course-service-ui`, a React SPA served by the same FastAPI app,
73
+ for browsing courses and managing membership in the browser.
74
+
75
+ See [`e2x_course_hub/contract/README.md`](e2x_course_hub/contract/README.md) for the
76
+ full contract reference, including the catalog/selection data model and how a spawn
77
+ plays out end to end.
78
+
79
+ ## Architecture
80
+
81
+ ```
82
+ e2x_course_hub/
83
+ ├── contract/ # Protocol boundary shared with the infrastructure spawner
84
+ │ └── providers.py # InfrastructureCatalogProvider, SpawnOfferingProvider
85
+ │ # (re-exports the catalog/selection models from schema/ below)
86
+ ├── schema/ # Pydantic models backing the contract and the API
87
+ │ ├── catalog.py # InfrastructureCatalogOptions and friends
88
+ │ ├── selection.py # SpawnSelection, SpawnOffering, CourseReference
89
+ │ ├── course.py # CourseConfig / TermConfig
90
+ │ └── types.py # SpawnRole, UserLike
91
+ ├── db/ # SQLAlchemy models + CourseRepository
92
+ ├── api/ # Business logic, framework-agnostic
93
+ │ ├── api.py # API — aggregates the sub-APIs below
94
+ │ ├── course_api.py # Course/term CRUD and environment configuration
95
+ │ ├── membership_api.py # Course membership (extends e2x-hub-rbac's MembershipAPI)
96
+ │ ├── infrastructure_api.py # Read-only access to the infrastructure catalog
97
+ │ ├── spawn_offering_api.py # Implements SpawnOfferingProvider for the spawner
98
+ │ └── *_permissions.py # Role → permission mappings per sub-API
99
+ ├── course_service/ # The FastAPI service
100
+ │ ├── fastapi_app.py # App wiring: routers, auth, static files, SPA route
101
+ │ ├── settings.py # ServiceSettings (extends settings.CoreSettings)
102
+ │ ├── whoami.py # Minimal standalone service: returns the caller's identity
103
+ │ └── {courses,terms,infrastructure,membership,me}/ # One router + schemas per resource
104
+ ├── loader.py # Composition root: settings → concrete API objects
105
+ └── settings.py # CoreSettings: hub API, DB, membership, infrastructure provider
106
+
107
+ course-service-ui/ # React 19 + TypeScript SPA, built with Vite
108
+ ```
109
+
110
+ ### The two sides
111
+
112
+ | | Course hub (this repo) | Infrastructure spawner |
113
+ |------------|-----------------------------------------------------------------------|----------------------------------------------------|
114
+ | Knows | courses, terms, users, roles (via `e2x-hub-rbac`), permissions | what can actually be run: image families/tags, resource tiers, profiles |
115
+ | Implements | `SpawnOfferingProvider` | `InfrastructureCatalogProvider` |
116
+ | Consumes | `InfrastructureCatalogProvider` | `SpawnOfferingProvider` |
117
+ | Never does | translates a selection to hardware | decides who is allowed to do what |
118
+
119
+ The spawner is a separate, pluggable package. The course hub discovers it at startup through
120
+ the `e2x_course_hub.infrastructure_catalog_providers` entry-point group, selected by
121
+ name via the `E2X_INFRASTRUCTURE_PROVIDER` setting — the hub never imports it directly.
122
+ See [`e2x-course-hub-kubespawner`](https://github.com/DigiKlausur/e2x-course-hub-kubespawner)
123
+ for an example implementation based on KubeSpawner.
124
+
125
+ ## Installation
126
+
127
+ ```bash
128
+ pip install git+https://github.com/Digiklausur/e2x-course-hub.git
129
+ ```
130
+
131
+ For development:
132
+
133
+ ```bash
134
+ git clone https://github.com/Digiklausur/e2x-course-hub
135
+ cd e2x-course-hub
136
+ pip install -e ".[dev]"
137
+ ```
138
+
139
+ Building the package (`pip install .` or `python -m build`) also builds
140
+ `course-service-ui` via `hatch-jupyter-builder` and bundles the result into the wheel.
141
+ For frontend-only iteration:
142
+
143
+ ```bash
144
+ cd course-service-ui
145
+ npm install
146
+ npm run dev
147
+ ```
148
+
149
+ ## Running it
150
+
151
+ The service is a standard [JupyterHub managed service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html),
152
+ run with `uvicorn`:
153
+
154
+ ```python
155
+ # jupyterhub_config.py
156
+ c.JupyterHub.services = [
157
+ {
158
+ "name": "course-service",
159
+ "url": "http://127.0.0.1:10105",
160
+ "command": ["uvicorn", "e2x_course_hub.course_service.fastapi_app:app"],
161
+ "environment": {
162
+ "UVICORN_HOST": "0.0.0.0",
163
+ "UVICORN_PORT": "10105",
164
+ "E2X_INFRASTRUCTURE_PROVIDER": "k8s_catalog",
165
+ "E2X_ADD_USERS_TO_HUB": "true",
166
+ "E2X_DELETE_EMPTY_GROUPS": "true",
167
+ "E2X_COURSE_DB_URL": "sqlite:////srv/jupyterhub/courses.db",
168
+ },
169
+ }
170
+ ]
171
+
172
+ c.JupyterHub.load_roles = [
173
+ {
174
+ "name": "course-service",
175
+ "services": ["course-service"],
176
+ "scopes": [
177
+ "access:services!service=course-service",
178
+ "admin:users",
179
+ "list:users",
180
+ "delete:users",
181
+ "admin:groups",
182
+ ],
183
+ }
184
+ ]
185
+ ```
186
+
187
+ `JUPYTERHUB_API_URL`, `JUPYTERHUB_API_TOKEN`, and `JUPYTERHUB_SERVICE_PREFIX` are
188
+ injected automatically by JupyterHub for a managed service and don't need to be set
189
+ explicitly.
190
+
191
+ ### Configuration
192
+
193
+ All settings are read from the environment (via `pydantic-settings`):
194
+
195
+ | Variable | Default | Purpose |
196
+ |-------------------------------|-----------------------------|-----------------------------------------------------------------|
197
+ | `JUPYTERHUB_API_URL` | — | JupyterHub Hub API URL, used to manage users and groups |
198
+ | `JUPYTERHUB_API_TOKEN` | — | API token for the above |
199
+ | `JUPYTERHUB_SERVICE_PREFIX` | `/` | URL prefix the service is mounted under |
200
+ | `E2X_COURSE_DB_URL` | `sqlite:///courses.db` | SQLAlchemy URL for the course/term database |
201
+ | `E2X_INFRASTRUCTURE_PROVIDER` | `default` | Entry-point name of the `InfrastructureCatalogProvider` to load |
202
+ | `E2X_ADD_USERS_TO_HUB` | `false` | Create JupyterHub users automatically on membership changes |
203
+ | `E2X_DELETE_EMPTY_GROUPS` | `false` | Delete a course/term's JupyterHub group once it has no members |
204
+
205
+ ### Optional: `whoami` service
206
+
207
+ `e2x_course_hub.course_service.whoami:app` is a minimal standalone FastAPI service that
208
+ returns the authenticated caller's identity — handy for verifying JupyterHub OAuth is
209
+ wired up correctly before debugging the full course service.
210
+
211
+ ## REST API
212
+
213
+ All routes are served under `{service_prefix}/v1`. Interactive docs (Swagger UI) are
214
+ available at `{service_prefix}/docs` once the service is running, and the schema can be
215
+ exported standalone with `python scripts/export_openapi.py`.
216
+
217
+ | Resource | Routes |
218
+ |----------------------|------------------------------------------------------------------------------------------------------------|
219
+ | Courses | `GET/POST /courses`, `GET/DELETE /courses/{course_id}`, `GET/PATCH /courses/{course_id}/metadata`, `GET/PATCH /courses/{course_id}/environment`, `GET /courses/{course_id}/terms` |
220
+ | Terms | `POST/GET/DELETE /courses/{course_id}/terms/{term_id}`, `GET/PATCH /courses/{course_id}/terms/{term_id}/environment` |
221
+ | Infrastructure | `GET /catalogs/image-catalog`, `GET /catalogs/resource-tiers`, `GET /catalogs/profile-catalog` |
222
+ | Membership | `GET/PATCH /lms/admins`, `GET/PATCH /lms/course-creators`, `GET/PATCH /courses/{course_id}/owners`, `GET/PATCH /courses/{course_id}/terms/{term_id}/{instructors,teaching-assistants,students,observers}` |
223
+ | Roles | `GET /roles/{role}/actions` |
224
+ | Current user | `GET /me` |
225
+
226
+ `course-service-ui` consumes this API with types generated from the OpenAPI schema
227
+ (`npm run generate:api-types` in `course-service-ui/`); CI fails if the committed types
228
+ drift from the schema.
229
+
230
+ ## Roles & permissions
231
+
232
+ Roles are defined by `e2x-hub-rbac` and scoped to the LMS, a course, or a term. Default
233
+ permissions for course/term management (`e2x_course_hub/api/course_permissions.py`):
234
+
235
+ | Role | Scope | Default permissions |
236
+ |---------------------|--------|----------------------------------------------------------------------------------------|
237
+ | LMS admin | LMS | Everything |
238
+ | Course creator | LMS | Create courses, view courses |
239
+ | Course owner | Course | Manage the course (metadata, environment, remove) and its terms (add/remove, environment) |
240
+ | Instructor | Term | View the course, manage the term (environment, remove) |
241
+ | Teaching assistant | Term | View the course and term |
242
+ | Observer | Term | View the course and term |
243
+ | Student | Term | (none — spawns only, no management permissions) |
244
+
245
+ Membership itself (who holds which role, per course/term) is managed separately through
246
+ the membership API, backed by JupyterHub groups. The membership, infrastructure catalog
247
+ and spawn permission tables, and the group names behind each role, are in
248
+ [`docs/source/reference/roles.md`](docs/source/reference/roles.md).
249
+
250
+ ## Development
251
+
252
+ ```bash
253
+ ruff check .
254
+ ruff format --check .
255
+ ```
256
+
257
+ ```bash
258
+ cd course-service-ui
259
+ npm run lint
260
+ npm run format
261
+ ```
262
+
263
+ CI (`.github/workflows/ci.yml`) runs Python linting, frontend linting/formatting, and
264
+ checks that the generated OpenAPI schema and TypeScript types are committed and in
265
+ sync with the routers.
266
+
267
+ ## License
268
+
269
+ MIT License — see [LICENSE](LICENSE) for details.