Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
8fbcdd8
wip
rubenthoms May 4, 2025
597946c
wip
rubenthoms May 5, 2025
74808b4
wip
rubenthoms May 5, 2025
dbe7aec
First working version
rubenthoms May 5, 2025
a9d3080
wip
rubenthoms May 5, 2025
36ac786
wip
rubenthoms May 6, 2025
d77ed6e
wip
rubenthoms May 6, 2025
aae82f6
Merge remote-tracking branch 'equinor/main' into spike-database
rubenthoms Jun 12, 2025
7757f15
fix: poetry lock file
rubenthoms Jun 12, 2025
b35d944
First implementations
rubenthoms Jun 13, 2025
34232b4
wip
rubenthoms Jun 16, 2025
62c8657
wip
rubenthoms Jun 16, 2025
a15a576
wip
rubenthoms Jun 16, 2025
0f5c275
wip
rubenthoms Jun 16, 2025
244df84
wip
rubenthoms Jun 17, 2025
4cb1a9d
wip
rubenthoms Jun 17, 2025
ebba9fa
wip
rubenthoms Jun 17, 2025
26d029b
wip
rubenthoms Jun 18, 2025
29e7c7a
Merge remote-tracking branch 'equinor/main' into persistence/manager
rubenthoms Jun 18, 2025
c1ed6c5
wip
rubenthoms Jun 18, 2025
21c78f0
wip
rubenthoms Jun 19, 2025
ed6f380
wip
rubenthoms Jun 20, 2025
a02ad4b
wip
rubenthoms Jun 20, 2025
f0475ed
wip
rubenthoms Jun 23, 2025
222c3d2
wip
rubenthoms Jun 23, 2025
12e7bd1
wip
rubenthoms Jun 23, 2025
526f6bb
wip
rubenthoms Jun 23, 2025
0864ea8
wip
rubenthoms Jun 24, 2025
d630597
wip
rubenthoms Jun 24, 2025
222bf20
wip
rubenthoms Jun 24, 2025
1440f37
wip
rubenthoms Jun 25, 2025
caaab05
wip
rubenthoms Jun 26, 2025
2e4c8b3
wip
rubenthoms Jun 27, 2025
76a50d0
wip
rubenthoms Jun 30, 2025
5add20b
wip
rubenthoms Jun 30, 2025
fe17cc8
wip
rubenthoms Jun 30, 2025
7379beb
wip
rubenthoms Jul 1, 2025
c3ffed6
Removed unused file
rubenthoms Jul 1, 2025
8b5f3fa
wip
rubenthoms Jul 1, 2025
0e52ad6
wip
rubenthoms Jul 2, 2025
93dcd43
wip
rubenthoms Jul 2, 2025
76b27d5
wip
rubenthoms Jul 2, 2025
06669ae
wip
rubenthoms Jul 2, 2025
4efca0f
wip
rubenthoms Jul 3, 2025
56f5f4a
Adjusted nginx to let SPA handle routing
rubenthoms Jul 3, 2025
5323d31
fix
rubenthoms Jul 3, 2025
9cee7e6
Previews for snapshots
rubenthoms Jul 3, 2025
4b7d81e
URL fix
rubenthoms Jul 3, 2025
681a02f
Adjust nginx config
rubenthoms Jul 3, 2025
43eeb75
Adjustments
rubenthoms Jul 3, 2025
e94e9db
Revert
rubenthoms Jul 3, 2025
164fefb
Multiple fixes in backend classes
rubenthoms Jul 4, 2025
3ca3d3b
Minor improvements
rubenthoms Jul 7, 2025
a85fbde
fix: formatting and linting
rubenthoms Jul 7, 2025
f6ab0ec
Removed unnecessary async
rubenthoms Jul 7, 2025
c34540f
fix: remove async for create functions
rubenthoms Jul 7, 2025
26129a3
fix: additional fixes related to partition keys and pydantic models
rubenthoms Jul 7, 2025
d20ca6c
WIP -- Show recently visited snapshots
Anders2303 Jul 4, 2025
a8fa191
Fixed snapshot navigation more smooth
Anders2303 Jul 7, 2025
f7dcae0
wip
rubenthoms Jul 7, 2025
121ff73
Wip: Renamed "!model" to "models"
Anders2303 Jul 8, 2025
16ce933
Resolved feedback
Anders2303 Jul 8, 2025
c86a13b
Change access log model to include snapshot owner id and refactored u…
Anders2303 Jul 8, 2025
297c7ba
Merge pull request #2 from Anders2303/persistence/manager--recent-sna…
rubenthoms Jul 8, 2025
a0b6dc1
Merge branch 'persistence/updates-after-recent-snapshots-PR' into per…
rubenthoms Jul 10, 2025
073074e
Adjustments after merging
rubenthoms Jul 10, 2025
527c974
Hide module close button for snapshots
rubenthoms Jul 24, 2025
d869dd4
wip
rubenthoms Jul 24, 2025
058ce08
Revert "wip"
rubenthoms Jul 28, 2025
b8f6750
wip
rubenthoms Jul 28, 2025
bdf3421
wip
rubenthoms Jul 28, 2025
751ffb3
wip
rubenthoms Jul 28, 2025
9d21808
Improved cosmos-db startup logic
rubenthoms Jul 29, 2025
42fc35c
Merge remote-tracking branch 'equinor/main' into persistence/manager
rubenthoms Aug 4, 2025
5db70e2
Intermediate merge result without ensemble polling
rubenthoms Aug 4, 2025
d3de259
wip
rubenthoms Aug 4, 2025
b7d09c1
wip
rubenthoms Aug 5, 2025
619eb1e
wip
rubenthoms Aug 5, 2025
ce296a0
Improved ensemble timestamps logic
rubenthoms Aug 6, 2025
aae40c3
Added comment
rubenthoms Aug 6, 2025
5eb3d2b
Improved logic
rubenthoms Aug 6, 2025
daaed23
Multiple improvements
rubenthoms Aug 7, 2025
b498d7b
Moved UserAvatar to framework components
Anders2303 Aug 12, 2025
72b7462
Fixed sizing inconsistency in user avatar states
Anders2303 Aug 12, 2025
14e56b8
WIP: Updated recent session/snapshot list
Anders2303 Aug 12, 2025
3397f72
Merge branch 'main' into persistence/manager--session-list-cards
Anders2303 Aug 12, 2025
8f09486
Added user graph user info endpoint
Anders2303 Aug 12, 2025
0106c1a
Made owner-line fetch graph user data
Anders2303 Aug 12, 2025
a3ff638
Added tooltip to session cards
Anders2303 Aug 12, 2025
a963b47
Added a refresh button to the recent session lists
Anders2303 Aug 12, 2025
06ee3a9
Merge remote-tracking branch 'equinor/main' into persistence/manager
rubenthoms Aug 27, 2025
8f76ff5
Added pagination to session endpoint. Clean up som api type names (Ch…
Anders2303 Aug 27, 2025
64cb460
WIP (Cherry picked from 29f167353bc3e16cbd1569fb4d271c0cec5918ce)
Anders2303 Aug 27, 2025
e55b95a
Added lower case collation handling for sessions (Cherry-pick 612a974…
Anders2303 Aug 27, 2025
10291b1
Added case-insensitive fields/utils to snapshots (Cherry picked from …
Anders2303 Aug 27, 2025
63af2e9
Added metadata to snapshot logs (Cherry picked from e5811a57ec3f44b90…
Anders2303 Aug 27, 2025
50fdbe5
Merge pull request #5 from Anders2303/persistence/manager--lowercase_…
rubenthoms Aug 27, 2025
e836602
Changes to database design - wip
rubenthoms Aug 27, 2025
b742b01
merge
Anders2303 Aug 28, 2025
22bb53a
Improving error messages and cleaning up code
rubenthoms Aug 28, 2025
7e48ee0
Multiple improvements
rubenthoms Aug 28, 2025
4e12d7f
WIP
Anders2303 Jul 11, 2025
39c4be2
Added "see more" button
Anders2303 Aug 28, 2025
9556d1a
Added infinite scroll table for session overview
Anders2303 Jul 21, 2025
97de989
WIP
Anders2303 Aug 28, 2025
beaf598
Bumped tanstack version
Anders2303 Aug 25, 2025
a536ea0
Added infinite scroll to sessions dialog
Anders2303 Aug 28, 2025
415e7f8
Added (WIP) snapshots list
Anders2303 Aug 28, 2025
c949721
Fixed mutation side-effect when replacing data
Anders2303 Aug 29, 2025
e62b599
Exposed openSnapshot to public
Anders2303 Sep 3, 2025
51a8856
Added title and date filtering to session modal
Anders2303 Sep 4, 2025
2998ded
Added filtering to snapshot overview. Tweaked filter logic for both
Anders2303 Sep 5, 2025
abdd64b
Improving error messages and cleaning up code
rubenthoms Aug 28, 2025
64181a7
Multiple improvements
rubenthoms Aug 28, 2025
dcae228
Made session description optional
Anders2303 Sep 3, 2025
a2ce81e
Made edit modal close on save
Anders2303 Sep 3, 2025
5151602
Fixed timing issue where outdated sessions would be persisted
Anders2303 Sep 3, 2025
8f71abe
Made the temporary session be properly removed from local store on su…
Anders2303 Sep 3, 2025
c9800dc
Merge branch 'persistence/manager--session-list-cards' into persisten…
Anders2303 Sep 5, 2025
6c5b6e0
Made the start page use a grid layout. Tweaked session card sizes
Anders2303 Sep 5, 2025
6e43be3
Merge branch 'persistence/manager--session-list-cards' into persisten…
Anders2303 Sep 5, 2025
bc826b3
Added url copy to snapshot overview
Anders2303 Sep 5, 2025
3bf4cd1
Made session/snapshot index use continuation-tokens instead of standa…
Anders2303 Sep 8, 2025
41bcd5f
Resolved type/lint issues
Anders2303 Sep 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
647 changes: 646 additions & 1 deletion backend_py/primary/poetry.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -

path_is_protected = True

if path_to_check in ["/login", "/auth-callback"] + self._unprotected_paths:
path_is_protected = False
for unprotected in ["/login", "/auth-callback"] + self._unprotected_paths:
if path_to_check.startswith(unprotected):
path_is_protected = False
break

if path_is_protected:

Expand Down
8 changes: 8 additions & 0 deletions backend_py/primary/primary/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@
DEFAULT_STALE_WHILE_REVALIDATE = 3600 * 24 # 24 hour
REDIS_USER_SESSION_URL = "redis://redis-user-session:6379"
REDIS_CACHE_URL = "redis://redis-cache:6379"

COSMOS_DB_PROD_CONNECTION_STRING = os.environ.get("WEBVIZ_DB_CONNECTION_STRING", None)
# pylint: disable=line-too-long
COSMOS_DB_EMULATOR_URI = "https://host.docker.internal:8081/"
COSMOS_DB_EMULATOR_KEY = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;"

PERSISTENCE_DB_NAME = "persistence"
DASHBOARDS_CONTAINER_NAME = "dashboards"
12 changes: 11 additions & 1 deletion backend_py/primary/primary/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from primary.auth.auth_helper import AuthHelper
from primary.auth.enforce_logged_in_middleware import EnforceLoggedInMiddleware
from primary.middleware.add_process_time_to_server_timing_middleware import AddProcessTimeToServerTimingMiddleware
from primary.services.database_access.setup_local_database import maybe_setup_local_database

from primary.middleware.add_browser_cache import AddBrowserCacheMiddleware
from primary.routers.dev.router import router as dev_router
Expand All @@ -32,6 +33,9 @@
from primary.routers.vfp.router import router as vfp_router
from primary.routers.well.router import router as well_router
from primary.routers.well_completions.router import router as well_completions_router
from primary.routers.persistence.sessions.router import router as sessions_router
from primary.routers.persistence.snapshots.router import router as snapshots_router
from primary.routers.persistence.snapshot_preview.router import router as snapshot_preview_router
from primary.services.utils.httpx_async_client_wrapper import HTTPX_ASYNC_CLIENT_WRAPPER
from primary.utils.azure_monitor_setup import setup_azure_monitor_telemetry
from primary.utils.exception_handlers import configure_service_level_exception_handlers
Expand All @@ -58,6 +62,9 @@

LOGGER = logging.getLogger(__name__)

# Setup Cosmos DB emulator database if running locally
maybe_setup_local_database()


def custom_generate_unique_id(route: APIRoute) -> str:
return f"{route.name}"
Expand Down Expand Up @@ -106,6 +113,9 @@ async def shutdown_event_async() -> None:
app.include_router(rft_router, prefix="/rft", tags=["rft"])
app.include_router(vfp_router, prefix="/vfp", tags=["vfp"])
app.include_router(dev_router, prefix="/dev", tags=["dev"], include_in_schema=False)
app.include_router(sessions_router, prefix="/sessions", tags=["sessions"])
app.include_router(snapshots_router, prefix="/snapshots", tags=["snapshots"])
app.include_router(snapshot_preview_router, prefix="/snapshot-preview", tags=["snapshot_preview"])

auth_helper = AuthHelper()
app.include_router(auth_helper.router)
Expand All @@ -120,7 +130,7 @@ async def shutdown_event_async() -> None:

# Add out custom middleware to enforce that user is logged in
# Also redirects to /login endpoint for some select paths
unprotected_paths = ["/logout", "/logged_in_user", "/alive", "/openapi.json"]
unprotected_paths = ["/logout", "/logged_in_user", "/alive", "/openapi.json", "/snapshot-preview"]
paths_redirected_to_login = ["/", "/alive_protected"]

app.add_middleware(
Expand Down
2 changes: 2 additions & 0 deletions backend_py/primary/primary/routers/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@


class UserInfo(BaseModel):
user_id: str
username: str
display_name: str | None = None
avatar_b64str: str | None = None
Expand Down Expand Up @@ -63,6 +64,7 @@ async def get_logged_in_user(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No user is logged in")

user_info = UserInfo(
user_id=authenticated_user.get_user_id(),
username=authenticated_user.get_username(),
avatar_b64str=None,
display_name=None,
Expand Down
47 changes: 41 additions & 6 deletions backend_py/primary/primary/routers/graph/router.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,71 @@
import logging

import httpx
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Query, Path

from primary.auth.auth_helper import AuthHelper
from primary.services.utils.authenticated_user import AuthenticatedUser
from primary.services.graph_access.graph_access import GraphApiAccess
from primary.services.service_exceptions import Service, AuthorizationError, ServiceRequestError

from .schemas import GraphUserPhoto

from . import schemas

LOGGER = logging.getLogger(__name__)

router = APIRouter()


@router.get("/user_info/{user_id_or_email}")
async def get_user_info(
authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
user_id_or_email: str = Path(description="User email, id or 'me' for the authenticated user"),
) -> schemas.GraphUser | None:
if not authenticated_user.has_graph_access_token():
raise AuthorizationError("User can't access Graph API", Service.GENERAL)

graph_api_access = GraphApiAccess(authenticated_user.get_graph_access_token())

try:
user_info = await graph_api_access.get_user_info(user_id_or_email)

if not user_info:
return None

return schemas.GraphUser(
id=user_info["id"],
display_name=user_info["displayName"],
principal_name=user_info["userPrincipalName"],
email=user_info["mail"],
)

except httpx.HTTPError as exc:
raise ServiceRequestError(
"Error while fetching user from Microsoft Graph API (HTTP error)", Service.GENERAL
) from exc
except httpx.InvalidURL as exc:
raise ServiceRequestError(
"Error while fetching user from Microsoft Graph API (HTTP error)", Service.GENERAL
) from exc


@router.get("/user_photo/")
async def get_user_photo(
# fmt:off
authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
user_email: str = Query(description="User email or 'me' for the authenticated user"),
user_id_or_email: str = Query(description="User email or 'me' for the authenticated user"),
# fmt:on
) -> GraphUserPhoto:
) -> schemas.GraphUserPhoto:
"""Get username, display name and avatar from Microsoft Graph API for a given user email"""

user_photo = GraphUserPhoto(
user_photo = schemas.GraphUserPhoto(
avatar_b64str=None,
)

if authenticated_user.has_graph_access_token():
graph_api_access = GraphApiAccess(authenticated_user.get_graph_access_token())
try:
avatar_b64str = await graph_api_access.get_user_profile_photo(user_email)
avatar_b64str = await graph_api_access.get_user_profile_photo(user_id_or_email)

user_photo.avatar_b64str = avatar_b64str
except httpx.HTTPError as exc:
Expand Down
7 changes: 7 additions & 0 deletions backend_py/primary/primary/routers/graph/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@

class GraphUserPhoto(BaseModel):
avatar_b64str: str | None = None


class GraphUser(BaseModel):
id: str
principal_name: str
display_name: str
email: str
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from primary.services.database_access.session_access.model import SessionDocument
from primary.services.database_access.session_access.types import SessionMetadata
from . import schemas


def to_api_session_metadata_summary(session: SessionDocument) -> schemas.SessionMetadataWithId:
return schemas.SessionMetadataWithId(
id=session.id,
title=session.metadata.title,
description=session.metadata.description,
createdAt=session.metadata.created_at.isoformat(),
updatedAt=session.metadata.updated_at.isoformat(),
version=session.metadata.version,
hash=session.metadata.hash,
)


def to_api_session_metadata(metadata: SessionMetadata) -> schemas.SessionMetadata:
return schemas.SessionMetadata(
title=metadata.title,
description=metadata.description,
createdAt=metadata.created_at.isoformat(),
updatedAt=metadata.updated_at.isoformat(),
version=metadata.version,
hash=metadata.hash,
)


def to_api_session_record(document: SessionDocument) -> schemas.SessionDocument:
return schemas.SessionDocument(
id=document.id,
ownerId=document.owner_id,
metadata=to_api_session_metadata(document.metadata),
content=document.content,
)


def to_api_session_index_page(
sessions: list[SessionDocument], continuation_token: str | None
) -> schemas.SessionIndexPage:
return schemas.SessionIndexPage(
continuation_token=continuation_token,
items=[to_api_session_metadata_summary(session) for session in sessions],
)
107 changes: 107 additions & 0 deletions backend_py/primary/primary/routers/persistence/sessions/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import logging
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Query

from primary.middleware.add_browser_cache import no_cache
from primary.services.database_access.session_access.session_access import SessionAccess
from primary.services.database_access.query_collation_options import SortDirection
from primary.auth.auth_helper import AuthHelper, AuthenticatedUser
from primary.services.database_access.session_access.types import NewSession, SessionUpdate, SessionSortBy

from . import schemas
from .converters import (
to_api_session_metadata,
to_api_session_record,
to_api_session_index_page,
)


LOGGER = logging.getLogger(__name__)
router = APIRouter()


@router.get("/sessions", response_model=schemas.SessionIndexPage)
@no_cache
async def get_sessions_metadata(
user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
# ! Must be named "cursor" or "page" to make hey-api generate infinite-queries
# ! When we've updated to the latest hey-api version, we can change this to something custom
cursor: None | str = Query(None),
sort_by: Optional[SessionSortBy] = Query(None, description="Sort the result by"),
sort_direction: Optional[SortDirection] = Query(SortDirection.ASC, description="Sort direction: 'asc' or 'desc'"),
limit: int = Query(10, ge=1, le=100, description="Limit the number of results"),
# ? Is this becoming too many args? Should we make a post-search endpoint instead?
filter_title: Optional[str] = Query(None, description="Filter results by title (case insensitive)"),
filter_updated_from: Optional[str] = Query(None, description="Filter results by date"),
filter_updated_to: Optional[str] = Query(None, description="Filter results by date"),
) -> schemas.SessionIndexPage:
access = SessionAccess.create(user.get_user_id())

async with access:
(items, cont_token) = await access.get_user_sessions_by_page_async(
continuation_token=cursor,
page_size=limit,
sort_by=sort_by,
sort_direction=sort_direction,
filter_title=filter_title,
filter_updated_from=filter_updated_from,
filter_updated_to=filter_updated_to,
)

return to_api_session_index_page(items, cont_token)


@router.get("/sessions/{session_id}", response_model=schemas.SessionDocument)
@no_cache
async def get_session(
session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
) -> schemas.SessionDocument:
access = SessionAccess.create(user.get_user_id())
async with access:
session = await access.get_session_by_id_async(session_id)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
return to_api_session_record(session)


@router.get("/sessions/metadata/{session_id}", response_model=schemas.SessionMetadata)
@no_cache
async def get_session_metadata(
session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
) -> schemas.SessionMetadata:
access = SessionAccess.create(user.get_user_id())
async with access:
metadata = await access.get_session_metadata_async(session_id)
if not metadata:
raise HTTPException(status_code=404, detail="Session metadata not found")
return to_api_session_metadata(metadata)


@router.post("/sessions", response_model=str)
async def create_session(
session: NewSession, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
) -> str:
access = SessionAccess.create(user.get_user_id())
async with access:
session_id = await access.insert_session_async(session)
return session_id


@router.put("/sessions/{session_id}", description="Updates a session object. Allows for partial update objects")
async def update_session(
session_id: str,
session_update: SessionUpdate,
user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
) -> schemas.SessionDocument:
access = SessionAccess.create(user.get_user_id())
async with access:
updated_session = await access.update_session_async(session_id, session_update)
return to_api_session_record(updated_session)


@router.delete("/sessions/{session_id}")
async def delete_session(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)) -> None:
access = SessionAccess.create(user.get_user_id())
async with access:
await access.delete_session_async(session_id)
27 changes: 27 additions & 0 deletions backend_py/primary/primary/routers/persistence/sessions/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Optional
from pydantic import BaseModel


class SessionMetadata(BaseModel):
title: str
description: Optional[str]
createdAt: str
updatedAt: str
version: int
hash: str


class SessionMetadataWithId(SessionMetadata):
id: str


class SessionDocument(BaseModel):
id: str
ownerId: str
metadata: SessionMetadata
content: str


class SessionIndexPage(BaseModel):
items: list[SessionMetadataWithId]
continuation_token: str | None
Loading