Skip to content

Commit 0a60354

Browse files
pgettabilalesi
authored andcommitted
Add accounting for memodel creation (#79)
* Add integration for me-model creation and accounting * Start adding session class * Create a OneShot class to simplify accounging usage * Fix types, other lint issues * Remove basePath from accounting API calls * Correctly provide jobId when cancelling a reservation * Make accounting base URL available in the client * Disable accounting integration in local dev * Add authz to the make reservation endpoint * Revert type fix for getOrgFromSelfUrl and getProjFromSelfUrl methods * Do not await while sending usage to accounting service * Remove unnecessary comments
1 parent f77d502 commit 0a60354

File tree

18 files changed

+292
-31
lines changed

18 files changed

+292
-31
lines changed

.deployment-envs/.env.production

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
NEXT_PUBLIC_ACCOUNTING_BASE_URL=https://www.openbraininstitute.org/api/accounting
12
NEXT_PUBLIC_NEXUS_URL=https://openbluebrain.com/api/nexus/v1
23
NEXT_PUBLIC_BLUE_NAAS_URL=https://www.openbraininstitute.org/api/bluenaas
34
NEXT_PUBLIC_CELL_SVC_BASE_URL=https://www.openbraininstitute.org/api/circuit

.deployment-envs/.env.staging

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
NEXT_PUBLIC_ACCOUNTING_BASE_URL=https://staging.openbraininstitute.org/api/accounting
12
NEXT_PUBLIC_NEXUS_URL=https://staging.openbraininstitute.org/api/nexus/v1
23
NEXT_PUBLIC_BLUE_NAAS_URL=https://staging.openbraininstitute.org/api/bluenaas
34
NEXT_PUBLIC_CELL_SVC_BASE_URL=https://staging.openbraininstitute.org/api/circuit
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { convertObjectKeystoCamelCase } from '@/util/object-keys-format';
2+
import { auth } from '@/auth';
3+
import { accountingBaseUrl } from '@/config';
4+
import authFetch from '@/authFetch';
5+
import { assertApiResponse } from '@/util/utils';
6+
7+
export const DELETE = async (
8+
request: Request,
9+
{ params }: { params: { reservationId: string } }
10+
) => {
11+
const session = await auth();
12+
13+
if (!session) {
14+
return new Response('Unauthorized', {
15+
status: 401,
16+
statusText: 'The supplied authentication is not authorized for this action',
17+
});
18+
}
19+
20+
const { reservationId } = params;
21+
22+
try {
23+
const res = await authFetch(`${accountingBaseUrl}/reservation/oneshot/${reservationId}`, {
24+
method: 'DELETE',
25+
headers: { 'Content-Type': 'application/json' },
26+
});
27+
28+
const resObj = assertApiResponse(res);
29+
30+
return Response.json(convertObjectKeystoCamelCase(resObj));
31+
} catch (error: unknown) {
32+
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
33+
34+
return new Response('Failed to cancel oneshot reservation', {
35+
status: 502,
36+
statusText: errorMessage,
37+
});
38+
}
39+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { OneshotReservation, ServiceType } from '@/types/accounting';
2+
import {
3+
convertObjectKeystoCamelCase,
4+
convertObjectKeysToSnakeCase,
5+
} from '@/util/object-keys-format';
6+
import { auth } from '@/auth';
7+
import { accountingBaseUrl } from '@/config';
8+
import authFetch from '@/authFetch';
9+
import { assertApiResponse } from '@/util/utils';
10+
import { getVirtualLabProjectUsers } from '@/services/virtual-lab/projects';
11+
12+
export const POST = async (request: Request) => {
13+
const reservationRequest = (await request.json()) as Omit<OneshotReservation, 'type' | 'userId'>;
14+
const session = await auth();
15+
16+
if (!session) {
17+
return new Response('Unauthorized', {
18+
status: 401,
19+
statusText: 'The supplied authentication is not authorized for this action',
20+
});
21+
}
22+
23+
const { virtualLabId, projectId } = reservationRequest;
24+
25+
const projectUsers = await getVirtualLabProjectUsers(virtualLabId, projectId);
26+
const projectUser = projectUsers.data.users.find((user) => user.id === session.user.id);
27+
if (!projectUser) {
28+
return new Response('Unauthorized', {
29+
status: 401,
30+
statusText: 'The supplied authentication is not authorized for this action',
31+
});
32+
}
33+
34+
try {
35+
const res = await authFetch(`${accountingBaseUrl}/reservation/oneshot`, {
36+
method: 'POST',
37+
headers: { 'Content-Type': 'application/json' },
38+
body: JSON.stringify(
39+
convertObjectKeysToSnakeCase({
40+
...reservationRequest,
41+
userId: session.user.id,
42+
type: ServiceType.Oneshot,
43+
})
44+
),
45+
});
46+
47+
const resObj = assertApiResponse(res);
48+
49+
return Response.json(convertObjectKeystoCamelCase(resObj));
50+
} catch (error: unknown) {
51+
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
52+
53+
return new Response('Failed to create oneshot reservation', {
54+
status: 502,
55+
statusText: errorMessage,
56+
});
57+
}
58+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { OneshotUsage } from '@/types/accounting';
2+
import {
3+
convertObjectKeystoCamelCase,
4+
convertObjectKeysToSnakeCase,
5+
} from '@/util/object-keys-format';
6+
import { auth } from '@/auth';
7+
import { accountingBaseUrl } from '@/config';
8+
import authFetch from '@/authFetch';
9+
import { assertApiResponse } from '@/util/utils';
10+
11+
export const POST = async (request: Request) => {
12+
const usage = (await request.json()) as OneshotUsage;
13+
const session = await auth();
14+
15+
if (!session) {
16+
return new Response('Unauthorized', {
17+
status: 401,
18+
statusText: 'The supplied authentication is not authorized for this action',
19+
});
20+
}
21+
22+
try {
23+
const res = await authFetch(`${accountingBaseUrl}/usage/oneshot`, {
24+
method: 'POST',
25+
headers: { 'Content-Type': 'application/json' },
26+
body: JSON.stringify(convertObjectKeysToSnakeCase(usage)),
27+
});
28+
29+
const resObj = assertApiResponse(res);
30+
31+
return Response.json(convertObjectKeystoCamelCase(resObj));
32+
} catch (error: unknown) {
33+
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
34+
35+
return new Response('Failed to send oneshot usage', {
36+
status: 502,
37+
statusText: errorMessage,
38+
});
39+
}
40+
};

src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/(pages)/notebooks/member/UserNotebookPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Notebook } from '@/util/virtual-lab/github';
1010
import fetchNotebooks from '@/util/virtual-lab/fetchNotebooks';
1111
import authFetch from '@/authFetch';
1212
import { notification } from '@/api/notifications';
13-
import { assertErrorMessage, assertVLApiResponse } from '@/util/utils';
13+
import { assertErrorMessage, assertApiResponse } from '@/util/utils';
1414
import { virtualLabApi } from '@/config';
1515

1616
function useDelayedLoading(initialValue = false, delay = 200) {
@@ -217,7 +217,7 @@ export default function UserNotebookPage({
217217
}
218218
);
219219

220-
const newNotebook = await assertVLApiResponse(notebookRes);
220+
const newNotebook = await assertApiResponse(notebookRes);
221221

222222
const newValidatedNotebooks = NotebooksArraySchema.parse(newNotebook.data);
223223
setNotebooks([

src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/(pages)/notebooks/member/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import UserNotebookPage from './UserNotebookPage';
33
import { ServerSideComponentProp } from '@/types/common';
44
import { fetchNotebook, Notebook } from '@/util/virtual-lab/github';
55
import { virtualLabApi } from '@/config';
6-
import { assertErrorMessage, assertVLApiResponse } from '@/util/utils';
6+
import { assertErrorMessage, assertApiResponse } from '@/util/utils';
77
import authFetch from '@/authFetch';
88

99
export default async function Notebooks({
@@ -18,7 +18,7 @@ export default async function Notebooks({
1818
`${virtualLabApi.url}/projects/${projectId}/notebooks/?page_size=100`
1919
);
2020

21-
const userNotebookData = await assertVLApiResponse(userNotebooksRes);
21+
const userNotebookData = await assertApiResponse(userNotebooksRes);
2222

2323
const notebooks = NotebooksArraySchema.parse(userNotebookData.data.results);
2424

src/components/VirtualLab/projects/VirtualLabProjectAdmin/ProjectBalanceCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useSetAtom } from 'jotai';
33

44
import EditIcon from '@/components/icons/Edit';
55
import useBalanceTransferModal from '@/hooks/virtual-labs/project';
6-
import { ProjectBalance } from '@/types/virtual-lab/accounting';
6+
import { ProjectBalance } from '@/types/accounting';
77
import { Project } from '@/types/virtual-lab/projects';
88
import {
99
projectBalanceAtomFamily,

src/components/VirtualLab/projects/VirtualLabProjectAdmin/ProjectJobReportList.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
projectJobReportsAtomFamily,
88
virtualLabProjectUsersAtomFamily,
99
} from '@/state/virtual-lab/projects';
10-
import { JobReport, ServiceSubtype } from '@/types/virtual-lab/accounting';
10+
import { JobReport, ServiceSubtype } from '@/types/accounting';
1111

1212
const { Column } = Table;
1313

@@ -62,7 +62,9 @@ const tableTheme = {
6262
};
6363

6464
const activityLabel: Record<ServiceSubtype, string> = {
65+
[ServiceSubtype.SingleCellBuild]: 'Build',
6566
[ServiceSubtype.SingleCellSim]: 'Simulate',
67+
[ServiceSubtype.SynaptomeBuild]: 'Build',
6668
[ServiceSubtype.SynaptomeSim]: 'Simulate',
6769
[ServiceSubtype.Storage]: 'Storage',
6870
// TODO: check if the following subtypes are still relevant and find better labels for them
@@ -76,7 +78,9 @@ function activityRenderFn(subtype: ServiceSubtype) {
7678
}
7779

7880
const scaleLabel: Record<ServiceSubtype, string> = {
81+
[ServiceSubtype.SingleCellBuild]: 'Single cell',
7982
[ServiceSubtype.SingleCellSim]: 'Single cell',
83+
[ServiceSubtype.SynaptomeBuild]: 'Synaptome',
8084
[ServiceSubtype.SynaptomeSim]: 'Synaptome',
8185
[ServiceSubtype.Storage]: 'Storage',
8286
// TODO: check if the following subtypes are still relevant and find better labels for them

src/components/build-section/virtual-lab/synaptome/molecules/SynaptomeConfigurationForm.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import { useSessionStorage } from '@/hooks/useSessionStorage';
4545
import { ExploreESHit, ExploreResource } from '@/types/explore-section/es';
4646
import { SIMULATION_COLORS } from '@/constants/simulate/single-neuron';
4747
import { validateFormula } from '@/api/bluenaas/validateSynapseGenerationFormula';
48+
import { OneshotSession } from '@/services/accounting';
49+
import { ServiceSubtype } from '@/types/accounting';
4850

4951
const label = (text: string) => (
5052
<span className="text-base font-semibold text-primary-8">{text}</span>
@@ -263,12 +265,21 @@ export default function SynaptomeConfigurationForm({ org, project, resource }: P
263265
brainLocation: resource.brainLocation,
264266
};
265267

266-
const resp = await fetch(resourceUrl, {
267-
method: 'POST',
268-
headers: createHeaders(session.accessToken),
269-
body: JSON.stringify(sanitizedResource),
268+
const accountingSession = new OneshotSession({
269+
projectId: project,
270+
virtualLabId: org,
271+
subtype: ServiceSubtype.SynaptomeBuild,
272+
count: 1,
270273
});
271274

275+
const resp = await accountingSession.useWith<Response>(() =>
276+
fetch(resourceUrl, {
277+
method: 'POST',
278+
headers: createHeaders(session.accessToken),
279+
body: JSON.stringify(sanitizedResource),
280+
})
281+
);
282+
272283
const newSynaptomeModel: Entity = await resp.json();
273284

274285
refreshSynaptomeModels();

0 commit comments

Comments
 (0)