Skip to content

Commit 81bea7b

Browse files
committed
DOC: Add '/project' route documentation
1 parent fa8b487 commit 81bea7b

File tree

4 files changed

+189
-42
lines changed

4 files changed

+189
-42
lines changed

src/fmu_settings_api/session.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,15 @@ async def create_session(
8080
user_fmu_directory: UserFMUDirectory,
8181
expire_seconds: int = settings.SESSION_EXPIRE_SECONDS,
8282
) -> str:
83-
"""Creates a new session and stores it to the storage backend."""
83+
"""Creates a new session and stores it to the storage backend.
84+
85+
Params:
86+
user_fmu_directory: The user .fmu directory instance
87+
expire_seconds: How long the session should be valid. Optional, defaulted.
88+
89+
Returns:
90+
The session id of the newly created session
91+
"""
8492
session_id = str(uuid4())
8593
now = datetime.now(UTC)
8694
expiration_duration = timedelta(seconds=expire_seconds)
@@ -96,18 +104,26 @@ async def create_session(
96104

97105
return session_id
98106

99-
async def get_session(
100-
self: Self, session_id: str
101-
) -> Session | ProjectSession | None:
102-
"""Get the session data for a session id."""
107+
async def get_session(self: Self, session_id: str) -> Session | ProjectSession:
108+
"""Get the session data for a session id.
109+
110+
Params:
111+
session_id: The session id being requested
112+
113+
Returns:
114+
The session, if it exists and is valid
115+
116+
Raises:
117+
SessionNotFoundError: If the session does not exist or is invalid
118+
"""
103119
session = await self._retrieve_session(session_id)
104120
if not session:
105-
return None
121+
raise SessionNotFoundError("No active session found")
106122

107123
now = datetime.now(UTC)
108124
if session.expires_at < now:
109125
await self.destroy_session(session_id)
110-
return None
126+
raise SessionNotFoundError("Invalid or expired session")
111127

112128
session.last_accessed = now
113129
await self._update_session(session_id, session)
@@ -135,11 +151,9 @@ async def add_fmu_project_to_session(
135151
The updated ProjectSession
136152
137153
Raises:
138-
SessionNotFoundError: If no session was found
154+
SessionNotFoundError: If no valid session was found
139155
"""
140156
session = await session_manager.get_session(session_id)
141-
if not session:
142-
raise SessionNotFoundError(f"No session with id {session_id} found")
143157

144158
if isinstance(session, ProjectSession):
145159
project_session = session
@@ -159,12 +173,10 @@ async def remove_fmu_project_from_session(session_id: str) -> Session:
159173
The updates session
160174
161175
Raises:
162-
SessionNotFoundError: If no session was found
176+
SessionNotFoundError: If no valid session was found
163177
"""
164178
maybe_project_session = await session_manager.get_session(session_id)
165179

166-
if maybe_project_session is None:
167-
raise SessionNotFoundError(f"No session with id {session_id} found")
168180
if isinstance(maybe_project_session, Session):
169181
return maybe_project_session
170182

src/fmu_settings_api/v1/responses.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
),
1313
"content": {
1414
"application/json": {
15-
"example": {"detail": "Not authorized"},
15+
"example": {
16+
"examples": [
17+
{"detail": "Not authorized"},
18+
],
19+
},
1620
},
1721
},
1822
},
@@ -24,7 +28,11 @@
2428
),
2529
"content": {
2630
"application/json": {
27-
"example": {"detail": "Permission denied creating user .fmu"},
31+
"example": {
32+
"examples": [
33+
{"detail": "Permission denied creating user .fmu"},
34+
],
35+
},
2836
},
2937
},
3038
},
@@ -55,7 +63,11 @@
5563
"description": "Something unexpected has happened",
5664
"content": {
5765
"application/json": {
58-
"example": {"detail": "{string content of exception}"},
66+
"example": {
67+
"examples": [
68+
{"detail": "{string content of exception}"},
69+
],
70+
},
5971
},
6072
},
6173
},
@@ -80,7 +92,12 @@
8092
"description": "Something unexpected has happened",
8193
"content": {
8294
"application/json": {
83-
"example": {"detail": "Session error: {string content of exception}"},
95+
"example": {
96+
"examples": [
97+
{"detail": "Session error: {string content of exception}"},
98+
{"detail": "{string content of exception}"},
99+
],
100+
},
84101
},
85102
},
86103
},

src/fmu_settings_api/v1/routes/project.py

Lines changed: 137 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Routes to add an FMU project to an existing session."""
22

33
from pathlib import Path
4+
from typing import Final
45

56
from fastapi import APIRouter, HTTPException, Response
67
from fmu.settings import find_nearest_fmu_directory, get_fmu_directory
@@ -17,13 +18,82 @@
1718
add_fmu_project_to_session,
1819
remove_fmu_project_from_session,
1920
)
21+
from fmu_settings_api.v1.responses import GetSessionResponses, Responses
2022

2123
router = APIRouter(prefix="/project", tags=["project"])
2224

25+
ProjectResponses: Final[Responses] = {
26+
403: {
27+
"description": (
28+
"The OS returned a permissions error while locating or creating .fmu"
29+
),
30+
"content": {
31+
"application/json": {
32+
"example": {
33+
"examples": [
34+
{"detail": "Permission denied locating .fmu"},
35+
{"detail": "Permission denied accessing .fmu at {path}"},
36+
{"detail": "Permission denied creating .fmu at {path}"},
37+
],
38+
},
39+
},
40+
},
41+
},
42+
404: {
43+
"description": (
44+
"The .fmu directory was unable to be found at or above a given path, or "
45+
"the requested path to create a project .fmu directory at does not exist."
46+
),
47+
"content": {
48+
"application/json": {
49+
"example": {
50+
"examples": [
51+
{"detail": "No .fmu directory found from {path}"},
52+
{"detail": "No .fmu directory found at {path}"},
53+
{"detail": "Path {path} does not exist"},
54+
],
55+
},
56+
},
57+
},
58+
},
59+
}
2360

24-
@router.get("/", response_model=FMUProject)
61+
ProjectExistsResponses: Final[Responses] = {
62+
409: {
63+
"description": (
64+
"A project .fmu directory already exist at a given location, or may "
65+
"possibly not be a directory, i.e. it may be a .fmu file."
66+
),
67+
"content": {
68+
"application/json": {
69+
"example": {
70+
"examples": [
71+
{"detail": ".fmu exists at {path} but is not a directory"},
72+
{"detail": ".fmu already exists at {path}"},
73+
],
74+
},
75+
},
76+
},
77+
},
78+
}
79+
80+
81+
@router.get(
82+
"/",
83+
response_model=FMUProject,
84+
summary="Returns the paths and configuration of the nearest project .fmu directory",
85+
description=(
86+
"If a project is not already attached to the session id it will be "
87+
"attached after a call to this route. If one is already attached this "
88+
"route will return data for the project .fmu directory again."
89+
),
90+
responses={
91+
**GetSessionResponses,
92+
**ProjectResponses,
93+
},
94+
)
2595
async def get_project(session: SessionDep) -> FMUProject:
26-
"""Returns the paths and configuration for the nearest project .fmu directory.
96+
"""Returns the paths and configuration of the nearest project .fmu directory.
2797
2898
This directory is searched for above the current working directory.
2999
@@ -62,7 +132,24 @@ async def get_project(session: SessionDep) -> FMUProject:
62132
raise HTTPException(status_code=500, detail=str(e)) from e
63133

64134

65-
@router.post("/", response_model=FMUProject)
135+
@router.post(
136+
"/",
137+
response_model=FMUProject,
138+
summary=(
139+
"Returns the path and configuration of the project .fmu directory at 'path'"
140+
),
141+
description=(
142+
"Used for when a user selects a project .fmu directory in a directory not "
143+
"found above the user's current working directory. Will overwrite the "
144+
"project .fmu directory attached to a session if one exists. If not, it is "
145+
"added to the session."
146+
),
147+
responses={
148+
**GetSessionResponses,
149+
**ProjectResponses,
150+
**ProjectExistsResponses,
151+
},
152+
)
66153
async def post_project(session: SessionDep, fmu_dir_path: FMUDirPath) -> FMUProject:
67154
"""Returns the paths and configuration for the project .fmu directory at 'path'."""
68155
path = fmu_dir_path.path
@@ -91,26 +178,23 @@ async def post_project(session: SessionDep, fmu_dir_path: FMUDirPath) -> FMUProj
91178
raise HTTPException(status_code=500, detail=str(e)) from e
92179

93180

94-
@router.delete("/", response_model=Message)
95-
async def delete_project_session(
96-
session: ProjectSessionDep, response: Response
97-
) -> Message:
98-
"""Deletes a project .fmu session if it exists."""
99-
try:
100-
await remove_fmu_project_from_session(session.id)
101-
return Message(
102-
message=(
103-
f"FMU directory {session.project_fmu_directory.path} closed "
104-
"successfully"
105-
),
106-
)
107-
except SessionNotFoundError as e:
108-
raise HTTPException(status_code=401, detail=str(e)) from e
109-
except Exception as e:
110-
raise HTTPException(status_code=500, detail=str(e)) from e
111-
112-
113-
@router.post("/init", response_model=FMUProject)
181+
@router.post(
182+
"/init",
183+
response_model=FMUProject,
184+
summary=(
185+
"Initializes a project .fmu directory at 'path' and returns its paths and "
186+
"configuration"
187+
),
188+
description=(
189+
"If a project .fmu directory is already attached to the session, this will "
190+
"switch to use the newly created .fmu directory."
191+
),
192+
responses={
193+
**GetSessionResponses,
194+
**ProjectResponses,
195+
**ProjectExistsResponses,
196+
},
197+
)
114198
async def init_project(
115199
session: SessionDep,
116200
fmu_dir_path: FMUDirPath,
@@ -142,3 +226,33 @@ async def init_project(
142226
) from e
143227
except Exception as e:
144228
raise HTTPException(status_code=500, detail=str(e)) from e
229+
230+
231+
@router.delete(
232+
"/",
233+
response_model=Message,
234+
summary="Removes a project .fmu directory from a session",
235+
description=(
236+
"This route simply removes an opened project .fmu directory from a session. "
237+
"It does not affect the user other aside from that."
238+
),
239+
responses={
240+
**GetSessionResponses,
241+
},
242+
)
243+
async def delete_project_session(
244+
session: ProjectSessionDep, response: Response
245+
) -> Message:
246+
"""Deletes a project .fmu session if it exists."""
247+
try:
248+
await remove_fmu_project_from_session(session.id)
249+
return Message(
250+
message=(
251+
f"FMU directory {session.project_fmu_directory.path} closed "
252+
"successfully"
253+
),
254+
)
255+
except SessionNotFoundError as e:
256+
raise HTTPException(status_code=401, detail=str(e)) from e
257+
except Exception as e:
258+
raise HTTPException(status_code=500, detail=str(e)) from e

tests/test_session.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from pathlib import Path
66
from unittest.mock import patch
77

8+
import pytest
89
from fmu.settings._init import init_user_fmu_directory
910

1011
from fmu_settings_api.config import settings
1112
from fmu_settings_api.session import (
1213
SessionManager,
14+
SessionNotFoundError,
1315
create_fmu_session,
1416
destroy_fmu_session,
1517
session_manager,
@@ -50,7 +52,8 @@ async def test_get_non_existing_session(
5052
"""Tests getting an existing session."""
5153
user_fmu_dir = init_user_fmu_directory()
5254
await session_manager.create_session(user_fmu_dir)
53-
assert await session_manager.get_session("no") is None
55+
with pytest.raises(SessionNotFoundError, match="No active session found"):
56+
await session_manager.get_session("no")
5457
assert len(session_manager.storage) == 1
5558

5659

@@ -77,7 +80,8 @@ async def test_get_existing_session_expiration(
7780

7881
# Pretend it expired a second ago.
7982
orig_session.expires_at = datetime.now(UTC) - timedelta(seconds=1)
80-
assert await session_manager.get_session(session_id) is None
83+
with pytest.raises(SessionNotFoundError, match="Invalid or expired session"):
84+
assert await session_manager.get_session(session_id)
8185
# It should also be destroyed.
8286
assert session_id not in session_manager.storage
8387
assert len(session_manager.storage) == 0

0 commit comments

Comments
 (0)