timeback-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. timeback_sdk-0.1.0/.gitignore +48 -0
  2. timeback_sdk-0.1.0/PKG-INFO +16 -0
  3. timeback_sdk-0.1.0/README.md +43 -0
  4. timeback_sdk-0.1.0/pyproject.toml +25 -0
  5. timeback_sdk-0.1.0/pytest.ini +6 -0
  6. timeback_sdk-0.1.0/src/timeback/__init__.py +135 -0
  7. timeback_sdk-0.1.0/src/timeback/py.typed +0 -0
  8. timeback_sdk-0.1.0/src/timeback/server/__init__.py +77 -0
  9. timeback_sdk-0.1.0/src/timeback/server/adapters/__init__.py +5 -0
  10. timeback_sdk-0.1.0/src/timeback/server/adapters/django.py +35 -0
  11. timeback_sdk-0.1.0/src/timeback/server/adapters/fastapi.py +170 -0
  12. timeback_sdk-0.1.0/src/timeback/server/handlers/__init__.py +24 -0
  13. timeback_sdk-0.1.0/src/timeback/server/handlers/activity.py +840 -0
  14. timeback_sdk-0.1.0/src/timeback/server/handlers/factory.py +79 -0
  15. timeback_sdk-0.1.0/src/timeback/server/handlers/identity.py +405 -0
  16. timeback_sdk-0.1.0/src/timeback/server/handlers/user.py +325 -0
  17. timeback_sdk-0.1.0/src/timeback/server/lib/__init__.py +29 -0
  18. timeback_sdk-0.1.0/src/timeback/server/lib/logger.py +59 -0
  19. timeback_sdk-0.1.0/src/timeback/server/lib/oidc.py +124 -0
  20. timeback_sdk-0.1.0/src/timeback/server/lib/resolve.py +412 -0
  21. timeback_sdk-0.1.0/src/timeback/server/lib/utils.py +70 -0
  22. timeback_sdk-0.1.0/src/timeback/server/timeback.py +461 -0
  23. timeback_sdk-0.1.0/src/timeback/server/types.py +567 -0
  24. timeback_sdk-0.1.0/src/timeback/shared/__init__.py +57 -0
  25. timeback_sdk-0.1.0/src/timeback/shared/constants.py +51 -0
  26. timeback_sdk-0.1.0/src/timeback/shared/types.py +285 -0
  27. timeback_sdk-0.1.0/tests/__init__.py +1 -0
  28. timeback_sdk-0.1.0/tests/test_activity_validation.py +311 -0
  29. timeback_sdk-0.1.0/tests/test_canonical_activity_url.py +242 -0
  30. timeback_sdk-0.1.0/tests/test_course_resolution.py +189 -0
  31. timeback_sdk-0.1.0/tests/test_identity_handler.py +219 -0
  32. timeback_sdk-0.1.0/tests/test_oidc.py +62 -0
  33. timeback_sdk-0.1.0/tests/test_resolve.py +163 -0
  34. timeback_sdk-0.1.0/tests/test_types.py +204 -0
  35. timeback_sdk-0.1.0/tests/test_user_handler.py +418 -0
  36. timeback_sdk-0.1.0/tests/test_utils.py +71 -0
@@ -0,0 +1,48 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+
22
+ # Virtual environments
23
+ .venv/
24
+ venv/
25
+ ENV/
26
+
27
+ # uv
28
+ uv.lock
29
+
30
+ # ruff
31
+ .ruff_cache/
32
+
33
+ # Testing
34
+ .pytest_cache/
35
+ .coverage
36
+ htmlcov/
37
+ .tox/
38
+ .nox/
39
+
40
+ # IDEs
41
+ .idea/
42
+ .vscode/
43
+ *.swp
44
+ *.swo
45
+
46
+ # OS
47
+ .DS_Store
48
+ Thumbs.db
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: timeback-sdk
3
+ Version: 0.1.0
4
+ Summary: Timeback SDK for Python - adapters for FastAPI, Django, and more
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27.0
7
+ Requires-Dist: starlette>=0.35.0
8
+ Requires-Dist: timeback-core
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
12
+ Requires-Dist: ruff>=0.8.0; extra == 'dev'
13
+ Provides-Extra: django
14
+ Requires-Dist: django>=4.0; extra == 'django'
15
+ Provides-Extra: fastapi
16
+ Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
@@ -0,0 +1,43 @@
1
+ # Timeback SDK
2
+
3
+ Server-side SDK for integrating Timeback into Python web applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # With FastAPI
9
+ pip install timeback[fastapi]
10
+
11
+ # With Django
12
+ pip install timeback[django]
13
+ ```
14
+
15
+ ## FastAPI
16
+
17
+ ```python
18
+ from fastapi import FastAPI
19
+ from timeback.fastapi import create_timeback_router
20
+
21
+ app = FastAPI()
22
+
23
+ timeback_router = create_timeback_router(
24
+ env="staging",
25
+ client_id="...",
26
+ client_secret="...",
27
+ identity={
28
+ "mode": "sso",
29
+ "client_id": "...",
30
+ "client_secret": "...",
31
+ "get_user": lambda req: get_session_user(req),
32
+ "on_callback_success": lambda ctx: handle_sso_success(ctx),
33
+ },
34
+ )
35
+
36
+ app.include_router(timeback_router, prefix="/api/timeback")
37
+ ```
38
+
39
+ ## Django
40
+
41
+ ```python
42
+ # Coming soon
43
+ ```
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "timeback-sdk"
3
+ version = "0.1.0"
4
+ description = "Timeback SDK for Python - adapters for FastAPI, Django, and more"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "timeback-core",
8
+ "httpx>=0.27.0",
9
+ "starlette>=0.35.0",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ fastapi = ["fastapi>=0.100.0"]
14
+ django = ["django>=4.0"]
15
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "ruff>=0.8.0"]
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/timeback"]
23
+
24
+ [tool.uv.sources]
25
+ timeback-core = { workspace = true }
@@ -0,0 +1,6 @@
1
+ [pytest]
2
+ asyncio_mode = auto
3
+ asyncio_default_fixture_loop_scope = function
4
+ testpaths = tests
5
+ python_files = test_*.py
6
+ python_functions = test_*
@@ -0,0 +1,135 @@
1
+ """
2
+ Timeback SDK for Python.
3
+
4
+ Server-side SDK for integrating Timeback into Python web applications.
5
+
6
+ Example (FastAPI with SSO - Full SDK):
7
+ ```python
8
+ from fastapi import FastAPI
9
+ from timeback import ApiCredentials, SsoIdentityConfig
10
+ from timeback.server import create_server, to_fastapi_router
11
+
12
+ async def create_app():
13
+ timeback = await create_server(TimebackConfig(
14
+ env="staging",
15
+ api=ApiCredentials(
16
+ client_id=os.environ["TIMEBACK_API_CLIENT_ID"],
17
+ client_secret=os.environ["TIMEBACK_API_CLIENT_SECRET"],
18
+ ),
19
+ identity=SsoIdentityConfig(
20
+ mode="sso",
21
+ client_id=os.environ["COGNITO_CLIENT_ID"],
22
+ client_secret=os.environ["COGNITO_CLIENT_SECRET"],
23
+ get_user=get_session_user, # returns { id: timebackId, email }
24
+ on_callback_success=handle_sso_success,
25
+ ),
26
+ ))
27
+
28
+ app = FastAPI()
29
+ app.include_router(to_fastapi_router(timeback), prefix="/api/timeback")
30
+ return app
31
+ ```
32
+
33
+ Example (FastAPI with custom auth):
34
+ ```python
35
+ from timeback import ApiCredentials, CustomIdentityConfig
36
+ from timeback.server import create_server, to_fastapi_router
37
+
38
+ timeback = await create_server(TimebackConfig(
39
+ env="staging",
40
+ api=ApiCredentials(client_id="...", client_secret="..."),
41
+ identity=CustomIdentityConfig(
42
+ mode="custom",
43
+ get_email=lambda req: get_session(req).email, # returns email string
44
+ ),
45
+ ))
46
+ ```
47
+
48
+ Example (FastAPI with SSO - Identity-Only):
49
+ ```python
50
+ from fastapi import FastAPI
51
+ from timeback import IdentityOnlyConfig, IdentityOnlySsoConfig
52
+ from timeback.server import create_identity_only_server, to_fastapi_router
53
+
54
+ timeback = create_identity_only_server(IdentityOnlyConfig(
55
+ env="staging",
56
+ identity=IdentityOnlySsoConfig(
57
+ mode="sso",
58
+ client_id=os.environ["COGNITO_CLIENT_ID"],
59
+ client_secret=os.environ["COGNITO_CLIENT_SECRET"],
60
+ on_callback_success=lambda ctx: ctx.redirect("/dashboard"),
61
+ ),
62
+ ))
63
+
64
+ app = FastAPI()
65
+ app.include_router(to_fastapi_router(timeback), prefix="/api/auth")
66
+ ```
67
+ """
68
+
69
+ from .server.types import (
70
+ ApiCredentials,
71
+ BuildStateContext,
72
+ CallbackErrorContext,
73
+ CallbackSuccessContext,
74
+ CustomIdentityConfig,
75
+ IdentityConfig,
76
+ IdentityOnlyCallbackSuccessContext,
77
+ IdentityOnlyConfig,
78
+ IdentityOnlySsoConfig,
79
+ IdpData,
80
+ OIDCTokens,
81
+ OIDCUserInfo,
82
+ SsoIdentityConfig,
83
+ TimebackConfig,
84
+ )
85
+ from .shared.types import (
86
+ ActivityCourseRef,
87
+ ActivityEndPayload,
88
+ ActivityMetrics,
89
+ ActivityParams,
90
+ ActivityResponse,
91
+ CourseCodeRef,
92
+ Environment,
93
+ IdentityClaims,
94
+ SubjectGradeCourseRef,
95
+ TimebackAuthUser,
96
+ TimebackIdentity,
97
+ TimebackProfile,
98
+ TimebackSessionUser,
99
+ TimebackUser,
100
+ TimebackUserResolutionErrorCode,
101
+ is_subject_grade_course_ref,
102
+ )
103
+
104
+ __all__ = [
105
+ "ActivityCourseRef",
106
+ "ActivityEndPayload",
107
+ "ActivityMetrics",
108
+ "ActivityParams",
109
+ "ActivityResponse",
110
+ "ApiCredentials",
111
+ "BuildStateContext",
112
+ "CallbackErrorContext",
113
+ "CallbackSuccessContext",
114
+ "CourseCodeRef",
115
+ "CustomIdentityConfig",
116
+ "Environment",
117
+ "IdentityClaims",
118
+ "IdentityConfig",
119
+ "IdentityOnlyCallbackSuccessContext",
120
+ "IdentityOnlyConfig",
121
+ "IdentityOnlySsoConfig",
122
+ "IdpData",
123
+ "OIDCTokens",
124
+ "OIDCUserInfo",
125
+ "SsoIdentityConfig",
126
+ "SubjectGradeCourseRef",
127
+ "TimebackAuthUser",
128
+ "TimebackConfig",
129
+ "TimebackIdentity",
130
+ "TimebackProfile",
131
+ "TimebackSessionUser",
132
+ "TimebackUser",
133
+ "TimebackUserResolutionErrorCode",
134
+ "is_subject_grade_course_ref",
135
+ ]
File without changes
@@ -0,0 +1,77 @@
1
+ """Timeback server SDK."""
2
+
3
+ from .adapters.fastapi import to_fastapi_router
4
+ from .handlers import IdentityOnlyHandlers, InvalidSensorUrlError, MissingSyncedCourseIdError
5
+ from .lib.resolve import (
6
+ ActivityCourseResolutionError,
7
+ TimebackUserResolutionError,
8
+ lookup_timeback_id_by_email,
9
+ resolve_activity_course,
10
+ resolve_status_for_user_resolution_error,
11
+ resolve_timeback_user_by_email,
12
+ )
13
+ from .timeback import (
14
+ AppConfig,
15
+ ConfigValidationError,
16
+ CourseEnvOverrides,
17
+ CourseOverrides,
18
+ IdentityOnlyInstance,
19
+ TimebackInstance,
20
+ create_identity_only_server,
21
+ create_server,
22
+ )
23
+ from .types import (
24
+ ActivityBeforeSendData,
25
+ ApiCredentials,
26
+ BuildStateContext,
27
+ CallbackErrorContext,
28
+ CallbackSuccessContext,
29
+ CustomIdentityConfig,
30
+ IdentityConfig,
31
+ IdentityOnlyCallbackSuccessContext,
32
+ IdentityOnlyConfig,
33
+ IdentityOnlySsoConfig,
34
+ IdpData,
35
+ OIDCTokens,
36
+ OIDCUserInfo,
37
+ SsoIdentityConfig,
38
+ TimebackConfig,
39
+ TimebackHooks,
40
+ )
41
+
42
+ __all__ = [
43
+ "ActivityBeforeSendData",
44
+ "ActivityCourseResolutionError",
45
+ "ApiCredentials",
46
+ "AppConfig",
47
+ "BuildStateContext",
48
+ "CallbackErrorContext",
49
+ "CallbackSuccessContext",
50
+ "ConfigValidationError",
51
+ "CourseEnvOverrides",
52
+ "CourseOverrides",
53
+ "CustomIdentityConfig",
54
+ "IdentityConfig",
55
+ "IdentityOnlyCallbackSuccessContext",
56
+ "IdentityOnlyConfig",
57
+ "IdentityOnlyHandlers",
58
+ "IdentityOnlyInstance",
59
+ "IdentityOnlySsoConfig",
60
+ "IdpData",
61
+ "InvalidSensorUrlError",
62
+ "MissingSyncedCourseIdError",
63
+ "OIDCTokens",
64
+ "OIDCUserInfo",
65
+ "SsoIdentityConfig",
66
+ "TimebackConfig",
67
+ "TimebackHooks",
68
+ "TimebackInstance",
69
+ "TimebackUserResolutionError",
70
+ "create_identity_only_server",
71
+ "create_server",
72
+ "lookup_timeback_id_by_email",
73
+ "resolve_activity_course",
74
+ "resolve_status_for_user_resolution_error",
75
+ "resolve_timeback_user_by_email",
76
+ "to_fastapi_router",
77
+ ]
@@ -0,0 +1,5 @@
1
+ """Server adapters."""
2
+
3
+ from .fastapi import to_fastapi_router
4
+
5
+ __all__ = ["to_fastapi_router"]
@@ -0,0 +1,35 @@
1
+ """
2
+ Django adapter for Timeback SDK.
3
+
4
+ Provides URL patterns and views for Django integration.
5
+
6
+ Status: Not yet implemented. Coming soon.
7
+
8
+ Example (future API):
9
+ ```python
10
+ # urls.py
11
+ from django.urls import path, include
12
+ from timeback.django import get_timeback_urls
13
+
14
+ urlpatterns = [
15
+ path("api/timeback/", include(get_timeback_urls(
16
+ env="staging",
17
+ api={"client_id": "...", "client_secret": "..."},
18
+ identity={"mode": "custom", "get_user": get_request_user},
19
+ ))),
20
+ ]
21
+ ```
22
+ """
23
+
24
+ # Django adapter implementation will be added when there's demand.
25
+ # Key considerations:
26
+ # - Django uses sync views by default (async views available in 3.1+)
27
+ # - Django has its own Request/Response objects (not Starlette)
28
+ # - Session handling is built-in via django.contrib.sessions
29
+ # - Authentication is built-in via django.contrib.auth
30
+ #
31
+ # Implementation approach:
32
+ # 1. Create view functions that wrap the core handlers
33
+ # 2. Convert Django HttpRequest to a common format
34
+ # 3. Convert handler responses back to Django HttpResponse
35
+ # 4. Provide both sync (using async_to_sync) and async views
@@ -0,0 +1,170 @@
1
+ """
2
+ FastAPI Adapter
3
+
4
+ Adapts Timeback handlers for FastAPI.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, cast, overload
10
+
11
+ from fastapi import APIRouter, Request
12
+
13
+ if TYPE_CHECKING:
14
+ from starlette.responses import Response
15
+
16
+ from ..handlers.factory import Handlers
17
+ from ..timeback import IdentityOnlyInstance, TimebackInstance
18
+
19
+
20
+ # Type for flexible input (full or identity-only instance)
21
+ AnyTimebackInstance = "TimebackInstance | IdentityOnlyInstance"
22
+
23
+
24
+ @overload
25
+ def to_fastapi_router(
26
+ timeback: TimebackInstance,
27
+ *,
28
+ callback_path: str | None = None,
29
+ ) -> APIRouter: ...
30
+
31
+
32
+ @overload
33
+ def to_fastapi_router(
34
+ timeback: IdentityOnlyInstance,
35
+ *,
36
+ callback_path: str | None = None,
37
+ ) -> APIRouter: ...
38
+
39
+
40
+ def to_fastapi_router(
41
+ timeback: TimebackInstance | IdentityOnlyInstance,
42
+ *,
43
+ callback_path: str | None = None,
44
+ ) -> APIRouter:
45
+ """
46
+ Convert Timeback instance to FastAPI router.
47
+
48
+ Supports both full SDK instances (from create_server()) and identity-only
49
+ instances (from create_identity_only_server()).
50
+
51
+ Args:
52
+ timeback: Timeback instance (full or identity-only)
53
+ callback_path: Custom callback path for OAuth redirects. If your IdP has
54
+ a pre-registered callback URL that differs from the SDK default
55
+ (/identity/callback), specify the path here. The path should be
56
+ relative to the router mount point (e.g., "/auth/callback").
57
+
58
+ Returns:
59
+ FastAPI APIRouter with Timeback endpoints
60
+
61
+ Example (Full SDK):
62
+ ```python
63
+ from timeback.server import create_server, TimebackConfig, SsoIdentityConfig
64
+ from timeback.server.adapters.fastapi import to_fastapi_router
65
+
66
+ timeback = await create_server(TimebackConfig(
67
+ env="staging",
68
+ api=ApiCredentials(client_id="...", client_secret="..."),
69
+ identity=SsoIdentityConfig(
70
+ mode="sso",
71
+ client_id="...",
72
+ client_secret="...",
73
+ get_user=get_session_user,
74
+ on_callback_success=lambda ctx: ctx.redirect("/"),
75
+ ),
76
+ ))
77
+
78
+ app = FastAPI()
79
+ app.include_router(to_fastapi_router(timeback), prefix="/api/timeback")
80
+ ```
81
+
82
+ Example (Identity-Only):
83
+ ```python
84
+ from timeback.server import (
85
+ create_identity_only_server,
86
+ IdentityOnlyConfig,
87
+ IdentityOnlySsoConfig,
88
+ )
89
+ from timeback.server.adapters.fastapi import to_fastapi_router
90
+
91
+ timeback = create_identity_only_server(IdentityOnlyConfig(
92
+ env="staging",
93
+ identity=IdentityOnlySsoConfig(
94
+ mode="sso",
95
+ client_id="...",
96
+ client_secret="...",
97
+ on_callback_success=lambda ctx: ctx.redirect("/dashboard"),
98
+ ),
99
+ ))
100
+
101
+ app = FastAPI()
102
+ app.include_router(to_fastapi_router(timeback), prefix="/api/auth")
103
+ ```
104
+
105
+ Example (Custom Callback Path):
106
+ ```python
107
+ # When your IdP requires a specific callback URL like:
108
+ # https://example.com/api/auth/sso/callback/timeback
109
+
110
+ timeback = create_identity_only_server(IdentityOnlyConfig(
111
+ env="staging",
112
+ identity=IdentityOnlySsoConfig(
113
+ mode="sso",
114
+ client_id="...",
115
+ client_secret="...",
116
+ redirect_uri="https://example.com/api/auth/sso/callback/timeback",
117
+ on_callback_success=lambda ctx: ctx.redirect("/dashboard"),
118
+ ),
119
+ ))
120
+
121
+ app = FastAPI()
122
+ app.include_router(
123
+ to_fastapi_router(timeback, callback_path="/sso/callback/timeback"),
124
+ prefix="/api/auth",
125
+ )
126
+ ```
127
+ """
128
+ router = APIRouter()
129
+ handle = timeback.handle
130
+
131
+ # Determine if this is a full instance or identity-only
132
+ is_full_instance = hasattr(handle, "activity") and hasattr(handle, "user")
133
+
134
+ # Register identity routes
135
+ @router.get("/identity/signin")
136
+ async def identity_signin(request: Request) -> Response:
137
+ return await handle.identity.sign_in(request)
138
+
139
+ # Default callback path
140
+ @router.get("/identity/callback")
141
+ async def identity_callback(request: Request) -> Response:
142
+ return await handle.identity.callback(request)
143
+
144
+ # Custom callback path (if provided and different from default)
145
+ if callback_path and callback_path != "/identity/callback":
146
+ # Normalize path to ensure it starts with /
147
+ normalized_path = callback_path if callback_path.startswith("/") else f"/{callback_path}"
148
+
149
+ @router.get(normalized_path)
150
+ async def identity_callback_custom(request: Request) -> Response:
151
+ return await handle.identity.callback(request)
152
+
153
+ @router.get("/identity/signout")
154
+ async def identity_signout(_request: Request) -> Response:
155
+ return handle.identity.sign_out()
156
+
157
+ # Register full SDK routes only for full instances
158
+ if is_full_instance:
159
+ # Cast to Handlers since we've confirmed it has activity and user
160
+ full_handle = cast("Handlers", handle)
161
+
162
+ @router.post("/activity", response_model=None)
163
+ async def submit_activity(request: Request) -> Response:
164
+ return await full_handle.activity(request)
165
+
166
+ @router.get("/user/me")
167
+ async def user_me(request: Request) -> Response:
168
+ return await full_handle.user.me(request)
169
+
170
+ return router
@@ -0,0 +1,24 @@
1
+ """SDK request handlers."""
2
+
3
+ from .activity import InvalidSensorUrlError, MissingSyncedCourseIdError, create_activity_handler
4
+ from .factory import Handlers, create_handlers
5
+ from .identity import (
6
+ IdentityHandlers,
7
+ IdentityOnlyHandlers,
8
+ create_identity_handlers,
9
+ create_identity_only_handlers,
10
+ )
11
+ from .user import create_user_handler
12
+
13
+ __all__ = [
14
+ "Handlers",
15
+ "IdentityHandlers",
16
+ "IdentityOnlyHandlers",
17
+ "InvalidSensorUrlError",
18
+ "MissingSyncedCourseIdError",
19
+ "create_activity_handler",
20
+ "create_handlers",
21
+ "create_identity_handlers",
22
+ "create_identity_only_handlers",
23
+ "create_user_handler",
24
+ ]