From 3ad9c270484a7679e3ed3f317ba87305e05f3f36 Mon Sep 17 00:00:00 2001 From: kevin-hashimoto Date: Tue, 26 Nov 2024 09:47:58 -0800 Subject: [PATCH 01/38] feat: retrieve compliance report chain --- .../lcfs/web/api/compliance_report/repo.py | 78 ++++++--- .../lcfs/web/api/compliance_report/schema.py | 5 + .../web/api/compliance_report/services.py | 16 +- .../lcfs/web/api/compliance_report/views.py | 17 +- frontend/src/hooks/useComplianceReports.js | 20 +-- .../EditViewComplianceReport.jsx | 20 ++- .../components/AssessmentCard.jsx | 164 ++++++++++++------ 7 files changed, 219 insertions(+), 101 deletions(-) diff --git a/backend/lcfs/web/api/compliance_report/repo.py b/backend/lcfs/web/api/compliance_report/repo.py index 71a26cb3d..194afb8d0 100644 --- a/backend/lcfs/web/api/compliance_report/repo.py +++ b/backend/lcfs/web/api/compliance_report/repo.py @@ -29,7 +29,6 @@ ) from lcfs.web.api.compliance_report.schema import ( ComplianceReportBaseSchema, - ComplianceReportSummarySchema, ComplianceReportSummaryUpdateSchema, ) from lcfs.db.models.compliance.ComplianceReportHistory import ComplianceReportHistory @@ -435,34 +434,61 @@ async def get_compliance_report_by_id(self, report_id: int, is_model: bool = Fal """ Retrieve a compliance report from the database by ID """ - result = ( - ( - await self.db.execute( - select(ComplianceReport) - .options( - joinedload(ComplianceReport.organization), - joinedload(ComplianceReport.compliance_period), - joinedload(ComplianceReport.current_status), - joinedload(ComplianceReport.summary), - joinedload(ComplianceReport.history).joinedload( - ComplianceReportHistory.status - ), - joinedload(ComplianceReport.history).joinedload( - ComplianceReportHistory.user_profile - ), - joinedload(ComplianceReport.transaction), - ) - .where(ComplianceReport.compliance_report_id == report_id) - ) + result = await self.db.execute( + select(ComplianceReport) + .options( + joinedload(ComplianceReport.organization), + joinedload(ComplianceReport.compliance_period), + joinedload(ComplianceReport.current_status), + joinedload(ComplianceReport.summary), + joinedload(ComplianceReport.history).joinedload( + ComplianceReportHistory.status + ), + joinedload(ComplianceReport.history).joinedload( + ComplianceReportHistory.user_profile + ), + joinedload(ComplianceReport.transaction), ) - .unique() - .scalars() - .first() + .where(ComplianceReport.compliance_report_id == report_id) ) + + compliance_report = result.scalars().unique().first() + + if not compliance_report: + return None + if is_model: - return result - else: - return ComplianceReportBaseSchema.model_validate(result) + return compliance_report + + return ComplianceReportBaseSchema.model_validate(compliance_report) + + @repo_handler + async def get_compliance_report_chain(self, group_uuid: str): + result = await self.db.execute( + select(ComplianceReport) + .options( + joinedload(ComplianceReport.organization), + joinedload(ComplianceReport.compliance_period), + joinedload(ComplianceReport.current_status), + joinedload(ComplianceReport.summary), + joinedload(ComplianceReport.history).joinedload( + ComplianceReportHistory.status + ), + joinedload(ComplianceReport.history).joinedload( + ComplianceReportHistory.user_profile + ), + joinedload(ComplianceReport.transaction), + ) + .where(ComplianceReport.compliance_report_group_uuid == group_uuid) + .order_by(ComplianceReport.version.desc()) # Ensure ordering by version + ) + + compliance_reports = result.scalars().unique().all() + + return [ + ComplianceReportBaseSchema.model_validate(report) + for report in compliance_reports + ] @repo_handler async def get_fuel_type(self, fuel_type_id: int) -> FuelType: diff --git a/backend/lcfs/web/api/compliance_report/schema.py b/backend/lcfs/web/api/compliance_report/schema.py index d427ee7d5..0f157be8b 100644 --- a/backend/lcfs/web/api/compliance_report/schema.py +++ b/backend/lcfs/web/api/compliance_report/schema.py @@ -160,6 +160,11 @@ class ComplianceReportBaseSchema(BaseSchema): has_supplemental: bool +class ChainedComplianceReportSchema(BaseSchema): + report: ComplianceReportBaseSchema + chain: Optional[List[ComplianceReportBaseSchema]] = [] + + class ComplianceReportCreateSchema(BaseSchema): compliance_period: str organization_id: int diff --git a/backend/lcfs/web/api/compliance_report/services.py b/backend/lcfs/web/api/compliance_report/services.py index b5490755b..a1bc239f7 100644 --- a/backend/lcfs/web/api/compliance_report/services.py +++ b/backend/lcfs/web/api/compliance_report/services.py @@ -201,22 +201,34 @@ def _mask_report_status(self, reports: List) -> List: @service_handler async def get_compliance_report_by_id( - self, report_id: int, apply_masking: bool = False - ) -> ComplianceReportBaseSchema: + self, report_id: int, apply_masking: bool = False, get_chain: bool = False + ): """Fetches a specific compliance report by ID.""" report = await self.repo.get_compliance_report_by_id(report_id) if report is None: raise DataNotFoundException("Compliance report not found.") + validated_report = ComplianceReportBaseSchema.model_validate(report) masked_report = ( self._mask_report_status([validated_report])[0] if apply_masking else validated_report ) + history_masked_report = self._mask_report_status_for_history( masked_report, apply_masking ) + if get_chain: + compliance_report_chain = await self.repo.get_compliance_report_chain( + report.compliance_report_group_uuid + ) + + return { + "report": history_masked_report, + "chain": compliance_report_chain, + } + return history_masked_report def _mask_report_status_for_history( diff --git a/backend/lcfs/web/api/compliance_report/views.py b/backend/lcfs/web/api/compliance_report/views.py index c25a8568f..de43c0c26 100644 --- a/backend/lcfs/web/api/compliance_report/views.py +++ b/backend/lcfs/web/api/compliance_report/views.py @@ -19,7 +19,9 @@ ComplianceReportBaseSchema, ComplianceReportListSchema, ComplianceReportSummarySchema, - ComplianceReportUpdateSchema, ComplianceReportSummaryUpdateSchema, + ChainedComplianceReportSchema, + ComplianceReportUpdateSchema, + ComplianceReportSummaryUpdateSchema, ) from lcfs.web.api.compliance_report.services import ComplianceReportServices from lcfs.web.api.compliance_report.summary_service import ( @@ -66,12 +68,12 @@ async def get_compliance_reports( pagination.filters.append( FilterModel(field="status", filter="Draft", filter_type="text", type="notEqual") ) - return await service.get_compliance_reports_paginated(pagination) + return await service.get_compliance_reports_paginated(pagination) @router.get( "/{report_id}", - response_model=ComplianceReportBaseSchema, + response_model=ChainedComplianceReportSchema, status_code=status.HTTP_200_OK, ) @view_handler([RoleEnum.GOVERNMENT]) @@ -80,12 +82,16 @@ async def get_compliance_report_by_id( report_id: int, service: ComplianceReportServices = Depends(), validate: ComplianceReportValidation = Depends(), -) -> ComplianceReportBaseSchema: +) -> ChainedComplianceReportSchema: await validate.validate_organization_access(report_id) mask_statuses = not user_has_roles(request.user, [RoleEnum.GOVERNMENT]) - return await service.get_compliance_report_by_id(report_id, mask_statuses) + result = await service.get_compliance_report_by_id( + report_id, mask_statuses, get_chain=True + ) + + return result @router.get( @@ -128,6 +134,7 @@ async def update_compliance_report_summary( report_id, summary_data ) + @view_handler(["*"]) @router.put( "/{report_id}", diff --git a/frontend/src/hooks/useComplianceReports.js b/frontend/src/hooks/useComplianceReports.js index 0a79e4023..c28f04fd4 100644 --- a/frontend/src/hooks/useComplianceReports.js +++ b/frontend/src/hooks/useComplianceReports.js @@ -55,7 +55,7 @@ export const useGetComplianceReport = (orgID, reportID, options) => { return useQuery({ queryKey: ['compliance-report', reportID], queryFn: async () => { - return (await client.get(path)) + return (await client.get(path)).data }, ...options }) @@ -132,22 +132,22 @@ export const useComplianceReportDocuments = (parentID, options) => { } export const useCreateSupplementalReport = (reportID, options) => { - const client = useApiService(); - const queryClient = useQueryClient(); - const path = apiRoutes.createSupplementalReport.replace(':reportID', reportID); + const client = useApiService() + const queryClient = useQueryClient() + const path = apiRoutes.createSupplementalReport.replace(':reportID', reportID) return useMutation({ mutationFn: () => client.post(path), onSuccess: (data) => { - queryClient.invalidateQueries(['compliance-reports']); + queryClient.invalidateQueries(['compliance-reports']) if (options && options.onSuccess) { - options.onSuccess(data); + options.onSuccess(data) } }, onError: (error) => { if (options && options.onError) { - options.onError(error); + options.onError(error) } - }, - }); -}; \ No newline at end of file + } + }) +} diff --git a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx index 28cc739ed..e05a6c55a 100644 --- a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx +++ b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx @@ -92,9 +92,9 @@ export const EditViewComplianceReport = () => { complianceReportId ) - const currentStatus = reportData?.data?.currentStatus?.status + const currentStatus = reportData?.report.currentStatus?.status const { data: orgData, isLoading } = useOrganization( - reportData?.data?.organizationId + reportData?.report.organizationId ) const { mutate: updateComplianceReport } = useUpdateComplianceReport( complianceReportId, @@ -126,7 +126,7 @@ export const EditViewComplianceReport = () => { t, setModalData, updateComplianceReport, - reportData, + isGovernmentUser, isSigningAuthorityDeclared }), @@ -136,7 +136,7 @@ export const EditViewComplianceReport = () => { t, setModalData, updateComplianceReport, - reportData, + isGovernmentUser, isSigningAuthorityDeclared ] @@ -182,7 +182,7 @@ export const EditViewComplianceReport = () => { {compliancePeriod + ' ' + t('report:complianceReport')} -{' '} - {reportData?.data?.nickname} + {reportData?.report.nickname} { )} {!location.state?.newReport && ( @@ -228,7 +229,10 @@ export const EditViewComplianceReport = () => { )} {!isGovernmentUser && ( - + )} {/* Internal Comments */} {isGovernmentUser && ( diff --git a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx index 65be99cc1..d5f2a055b 100644 --- a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx +++ b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx @@ -1,38 +1,69 @@ -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { List, ListItemText, Stack, Typography } from '@mui/material' +import BCButton from '@/components/BCButton' +import BCTypography from '@/components/BCTypography' import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard' -import { timezoneFormatter } from '@/utils/formatters' +import { Role } from '@/components/Role' +import { StyledListItem } from '@/components/StyledListItem' +import { roles } from '@/constants/roles' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' +import { useCreateSupplementalReport } from '@/hooks/useComplianceReports' +import { useCurrentUser } from '@/hooks/useCurrentUser' import { constructAddress } from '@/utils/constructAddress' -import BCButton from '@/components/BCButton' +import { timezoneFormatter } from '@/utils/formatters' import AssignmentIcon from '@mui/icons-material/Assignment' -import { useCreateSupplementalReport } from '@/hooks/useComplianceReports' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { List, ListItemText, Stack, Typography, styled } from '@mui/material' +import MuiAccordion from '@mui/material/Accordion' +import MuiAccordionDetails from '@mui/material/AccordionDetails' +import MuiAccordionSummary, { + accordionSummaryClasses +} from '@mui/material/AccordionSummary' import Box from '@mui/material/Box' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { StyledListItem } from '@/components/StyledListItem' -import { roles } from '@/constants/roles' -import { Role } from '@/components/Role' -export const AssessmentCard = ({ - orgData, - hasMet, - history, - hasSupplemental, - isGovernmentUser, - currentStatus, - complianceReportId, - alertRef -}) => { - const { t } = useTranslation(['report']) - const navigate = useNavigate() +const Accordion = styled((props) => ( + +))(() => ({ + border: `none`, + '&::before': { + display: 'none' + } +})) + +const AccordionSummary = styled((props) => ( + } + {...props} + /> +))(() => ({ + minHeight: 'unset', + padding: 0, + flexDirection: 'row-reverse', + [`& .${accordionSummaryClasses.content}`]: { + margin: 0 + }, + [`& .${accordionSummaryClasses.expanded}`]: { + margin: 0 + } +})) +const AccordionDetails = styled(MuiAccordionDetails)(() => ({ + paddingLeft: '1rem', + paddingTop: 0, + paddingBottom: 0 +})) + +const HistoryCard = ({ report }) => { + const { data: currentUser } = useCurrentUser() + const isGovernmentUser = currentUser?.isGovernmentUser + const { t } = useTranslation(['report']) const filteredHistory = useMemo(() => { - if (!history || history.length === 0) { + if (!report.history || report.history.length === 0) { return [] } // Sort the history array by date in descending order - return [...history] + return [...report.history] .sort((a, b) => { return new Date(b.createDate) - new Date(a.createDate) }) @@ -48,9 +79,62 @@ export const AssessmentCard = ({ .filter( (item) => item.status.status !== COMPLIANCE_REPORT_STATUSES.DRAFT || - hasSupplemental + report.hasSupplemental ) - }, [history, isGovernmentUser, hasSupplemental]) + }, [isGovernmentUser, report.hasSupplemental, report.history]) + return ( + + } + aria-controls="panel1-content" + > + + {report.version === 0 + ? `${report.compliancePeriod.description} Compliance Report` + : report.nickname} + : {report.currentStatus.status} + + + + + {filteredHistory.map((item, index) => ( + + + + + + ))} + + + + ) +} + +export const AssessmentCard = ({ + orgData, + hasMet, + hasSupplemental, + isGovernmentUser, + currentStatus, + complianceReportId, + alertRef, + chain +}) => { + const { t } = useTranslation(['report']) + const navigate = useNavigate() const { mutate: createSupplementalReport, isLoading } = useCreateSupplementalReport(complianceReportId, { @@ -165,7 +249,7 @@ export const AssessmentCard = ({ )} - {!!filteredHistory.length && ( + {!!chain.length && ( <> {t(`report:reportHistory`)} - - {filteredHistory.map((item, index) => ( - - - - - - ))} - + {chain.map((report) => ( + + ))} )} + {currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED && ( <> From 2000533afb6f501c7bb5b47e0025926ee7ec255a Mon Sep 17 00:00:00 2001 From: kevin-hashimoto Date: Mon, 2 Dec 2024 15:16:59 -0800 Subject: [PATCH 02/38] fix: backend test --- .../compliance_report/test_compliance_report_views.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/lcfs/tests/compliance_report/test_compliance_report_views.py b/backend/lcfs/tests/compliance_report/test_compliance_report_views.py index 9741e568d..aa5ca7675 100644 --- a/backend/lcfs/tests/compliance_report/test_compliance_report_views.py +++ b/backend/lcfs/tests/compliance_report/test_compliance_report_views.py @@ -12,6 +12,7 @@ from lcfs.web.api.compliance_report.schema import ( ComplianceReportUpdateSchema, ComplianceReportSummaryUpdateSchema, + ChainedComplianceReportSchema, ) from lcfs.services.s3.client import DocumentService @@ -226,7 +227,9 @@ async def test_get_compliance_report_by_id_success( ) as mock_validate_organization_access: set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) - mock_compliance_report = compliance_report_base_schema() + mock_compliance_report = ChainedComplianceReportSchema( + report=compliance_report_base_schema(), chain=[] + ) mock_get_compliance_report_by_id.return_value = mock_compliance_report mock_validate_organization_access.return_value = None @@ -240,7 +243,9 @@ async def test_get_compliance_report_by_id_success( expected_response = json.loads(mock_compliance_report.json(by_alias=True)) assert response.json() == expected_response - mock_get_compliance_report_by_id.assert_called_once_with(1, False) + mock_get_compliance_report_by_id.assert_called_once_with( + 1, False, get_chain=True + ) mock_validate_organization_access.assert_called_once_with(1) From e3a9afc777d1a6f700c9a7bd44c91264d232c6c6 Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Mon, 2 Dec 2024 23:12:10 -0800 Subject: [PATCH 03/38] fix: front end tests --- .../EditViewComplianceReports.test.jsx | 50 +++++++++----- .../__tests__/AssessmentCard.test.jsx | 68 +++++++++++++++++-- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx b/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx index 48700858c..cb50dbee6 100644 --- a/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx +++ b/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx @@ -91,10 +91,11 @@ describe('EditViewComplianceReport', () => { }, complianceReport: { data: { - data: { + report: { organizationId: '123', currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT } - } + }, + chain: [] }, isLoading: false, isError: false @@ -192,9 +193,10 @@ describe('EditViewComplianceReport', () => { setupMocks({ complianceReport: { data: { - data: { + report: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } - } + }, + chain: [] } }, currentUser: { @@ -214,11 +216,12 @@ describe('EditViewComplianceReport', () => { setupMocks({ complianceReport: { data: { - data: { + report: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_ANALYST } - } + }, + chain: [] } }, currentUser: { @@ -241,11 +244,12 @@ describe('EditViewComplianceReport', () => { setupMocks({ complianceReport: { data: { - data: { + report: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_MANAGER } - } + }, + chain: [] } }, currentUser: { @@ -268,9 +272,10 @@ describe('EditViewComplianceReport', () => { setupMocks({ complianceReport: { data: { - data: { + report: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED } - } + }, + chain: [] } }, currentUser: { @@ -290,9 +295,10 @@ describe('EditViewComplianceReport', () => { setupMocks({ complianceReport: { data: { - data: { + report: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } - } + }, + chain: [] } }, currentUser: { data: { isGovernmentUser: false }, hasRoles: () => false } @@ -340,7 +346,10 @@ describe('EditViewComplianceReport', () => { setupMocks({ complianceReport: { data: { - data: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT } } + report: { + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT } + }, + chain: [] } } }) @@ -366,10 +375,20 @@ describe('EditViewComplianceReport', () => { vi.mocked(useComplianceReportsHook.useGetComplianceReport).mockReturnValue({ data: { - data: { + report: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED }, history: historyMock - } + }, + chain: [ + { + history: historyMock, + version: 0, + compliancePeriod: { + description: '2024' + }, + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + } + ] }, isLoading: false, isError: false @@ -400,5 +419,4 @@ describe('EditViewComplianceReport', () => { expect(screen.getByLabelText('scroll to top')).toBeInTheDocument() }) }) - }) diff --git a/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx b/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx index dbecd910e..0c458eb19 100644 --- a/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx +++ b/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx @@ -77,12 +77,12 @@ describe('AssessmentCard', () => { , { wrapper } ) @@ -96,12 +96,12 @@ describe('AssessmentCard', () => { , { wrapper } ) @@ -112,16 +112,27 @@ describe('AssessmentCard', () => { }) it('renders report history when history is available', async () => { + const mockChain = [ + { + history: mockHistory, + version: 0, + compliancePeriod: { + description: '2024' + }, + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + } + ] + render( , { wrapper } ) @@ -146,16 +157,28 @@ describe('AssessmentCard', () => { } ] + const mockChain = [ + { + history: historyWithDraft, + version: 0, + compliancePeriod: { + description: '2024' + }, + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED }, + hasSupplemental: true + } + ] + render( , { wrapper } ) @@ -174,16 +197,27 @@ describe('AssessmentCard', () => { } ] + const mockChain = [ + { + history: historyWithDraft, + version: 0, + compliancePeriod: { + description: '2024' + }, + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + } + ] + render( , { wrapper } ) @@ -201,16 +235,26 @@ describe('AssessmentCard', () => { }) it('changes status to "AssessedBy" when the user is not a government user', async () => { + const mockChain = [ + { + history: mockHistory, + version: 0, + compliancePeriod: { + description: '2024' + }, + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED } + } + ] render( , { wrapper } ) @@ -222,16 +266,26 @@ describe('AssessmentCard', () => { }) it('displays organization information', async () => { + const mockChain = [ + { + history: mockHistory, + version: 0, + compliancePeriod: { + description: '2024' + }, + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED } + } + ] render( , { wrapper } ) From 94d6710b3dc269ac25df757aba05a17c86245162 Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 3 Dec 2024 03:59:54 -0800 Subject: [PATCH 04/38] Feat: LCFS - Pills in AG Grid display tables #1249 --- frontend/src/utils/grid/cellRenderers.jsx | 318 ++++++++++++------ .../Admin/AdminMenu/components/Users.jsx | 15 - .../Admin/AdminMenu/components/_schema.js | 2 - .../ViewOrganization/ViewOrganization.jsx | 16 - 4 files changed, 218 insertions(+), 133 deletions(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index 60ad2e761..aaea3d5dd 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -5,8 +5,8 @@ import { getAllFuelCodeStatuses, getAllOrganizationStatuses } from '@/constants/statuses' -import { Stack } from '@mui/material' import { Link, useLocation } from 'react-router-dom' +import { useState, useRef, useEffect } from 'react' export const TextRenderer = (props) => { return ( @@ -173,59 +173,6 @@ export const FuelCodeStatusTextRenderer = (props) => { ) } -export const CommonArrayRenderer = (props) => { - const location = useLocation() - const options = Array.isArray(props.value) - ? props.value - : props.value.split(',') - const chipContent = ( - - {options.map((mode) => ( - - ))} - - ) - return props.disableLink ? ( - chipContent - ) : ( - - {chipContent} - - ) -} - export const TransactionStatusRenderer = (props) => { const statusArr = [ 'Draft', @@ -321,52 +268,6 @@ export const ReportsStatusRenderer = (props) => { ) } -// if the status of the user is in-active then don't show their previously held roles -export const RoleRenderer = (props) => { - const location = useLocation() - return ( - - - - {props.data.isActive && - props.data.roles - .filter( - (r) => r.name !== roles.government && r.name !== roles.supplier - ) - .map((role) => ( - - ))}{' '} - - - - ) -} - export const RoleSpanRenderer = (props) => ( <> {props.data.roles @@ -390,3 +291,220 @@ export const RoleSpanRenderer = (props) => ( ))} ) + +const GenericChipRenderer = ({ + value, + disableLink = false, + renderChip = defaultRenderChip, + renderOverflowChip = defaultRenderOverflowChip, + chipConfig = {} +}) => { + const location = useLocation() + const containerRef = useRef(null) + const [visibleChips, setVisibleChips] = useState([]) + const [hiddenChipsCount, setHiddenChipsCount] = useState(0) + + const options = Array.isArray(value) + ? value + : value.split(',').map(item => item.trim()).filter(Boolean) + + useEffect(() => { + if (!containerRef.current) return + + const containerWidth = containerRef.current.offsetWidth + let totalWidth = 0 + const chipWidths = [] + + for (let i = 0; i < options.length; i++) { + const chipText = options[i] + const chipTextWidth = chipText.length * 6 // Assuming 6 pixels per character + const newTotalWidth = totalWidth + chipTextWidth + 32 // Add 32px for padding + + if (newTotalWidth <= containerWidth) { + chipWidths.push({ text: chipText, width: chipTextWidth + 32, ...chipConfig }) + totalWidth = newTotalWidth + } else { + // Stop and calculate remaining chips + setVisibleChips(chipWidths) + setHiddenChipsCount(options.length - chipWidths.length) + return + } + } + + // If all chips fit + setVisibleChips( + options.map((text) => ({ text, width: text.length * 6 + 32, ...chipConfig })) + ) + setHiddenChipsCount(0) + }, [chipConfig, options]) + + const chipContent = ( +
+ {visibleChips.map(renderChip)} + {renderOverflowChip(hiddenChipsCount)} +
+ ) + + return disableLink ? ( + chipContent + ) : ( + + {chipContent} + + ) +} + +// Default Render Chip Function for CommonArrayRenderer +const defaultRenderChip = (chip) => ( + + {chip.text} + +) + +// Default Overflow Chip for CommonArrayRenderer +const defaultRenderOverflowChip = (hiddenChipsCount) => + hiddenChipsCount > 0 && ( + + +{hiddenChipsCount} + + ) + +// Role Specific Render Chip Function +const roleRenderChip = (chip, isGovernmentRole = false) => ( + +) + +// Role Specific Overflow Chip +const roleRenderOverflowChip = (hiddenChipsCount, isGovernmentRole = false) => + hiddenChipsCount > 0 && ( + + +{hiddenChipsCount} + + ) + +export default GenericChipRenderer + +// Updated CommonArrayRenderer +export const CommonArrayRenderer = (props) => ( + +) + +export const RoleRenderer = (props) => { + const { value } = props + const [isGovernmentRole, setIsGovernmentRole] = useState(false) + + const filteredRoles = Array.isArray(value) + ? value + : value.split(',').map((role) => role.trim()).filter( + role => role !== roles.government && role !== roles.supplier + ) + + useEffect(() => { + setIsGovernmentRole( + Array.isArray(value) + ? value.includes(roles.government) + : value.includes(roles.government) + ) + }, [value]) + + return ( + roleRenderChip(chip, isGovernmentRole)} + renderOverflowChip={(count) => roleRenderOverflowChip(count, isGovernmentRole)} + /> + ) +} \ No newline at end of file diff --git a/frontend/src/views/Admin/AdminMenu/components/Users.jsx b/frontend/src/views/Admin/AdminMenu/components/Users.jsx index bcd65bc76..28f2e021b 100644 --- a/frontend/src/views/Admin/AdminMenu/components/Users.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/Users.jsx @@ -51,19 +51,6 @@ export const Users = () => { }) const gridRef = useRef() - const getRowHeight = useCallback((params) => { - const actualWidth = params.api.getColumn('role').getActualWidth() - return calculateRowHeight(actualWidth, params.data?.roles) - }, []) - - const onColumnResized = useCallback((params) => { - const actualWidth = params.api.getColumn('role').getActualWidth() - params.api.resetRowHeights() - params.api.forEachNode((node) => { - const rowHeight = calculateRowHeight(actualWidth, node.data?.roles) - node.setRowHeight(rowHeight) - }) - }, []) useEffect(() => { if (location.state?.message) { setAlertMessage(location.state.message) @@ -115,8 +102,6 @@ export const Users = () => { handleRowClicked={handleRowClicked} enableResetButton={false} enableCopyButton={false} - getRowHeight={getRowHeight} - onColumnResized={onColumnResized} />
diff --git a/frontend/src/views/Admin/AdminMenu/components/_schema.js b/frontend/src/views/Admin/AdminMenu/components/_schema.js index 23a63525f..7e9f3a303 100644 --- a/frontend/src/views/Admin/AdminMenu/components/_schema.js +++ b/frontend/src/views/Admin/AdminMenu/components/_schema.js @@ -28,8 +28,6 @@ export const usersColumnDefs = (t) => [ params.data.isActive ? params.data.roles.map((role) => role.name).join(', ') : '', - flex: 1, - minWidth: 300, sortable: false, suppressHeaderMenuButton: true, filterParams: { diff --git a/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx b/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx index 4cc1abd8b..2bf32af03 100644 --- a/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx +++ b/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx @@ -75,20 +75,6 @@ export const ViewOrganization = () => { } }, []) - const getRowHeight = useCallback((params) => { - const actualWidth = params.api.getColumn('role').getActualWidth() - return calculateRowHeight(actualWidth, params.data?.roles) - }, []) - - const onColumnResized = useCallback((params) => { - const actualWidth = params.api.getColumn('role').getActualWidth() - params.api.resetRowHeights() - params.api.forEachNode((node) => { - const rowHeight = calculateRowHeight(actualWidth, node.data?.roles) - node.setRowHeight(rowHeight) - }) - }, []) - const gridOptions = { overlayNoRowsTemplate: 'No users found', includeHiddenColumnsInQuickFilter: true @@ -348,8 +334,6 @@ export const ViewOrganization = () => { handleRowClicked={handleRowClicked} enableCopyButton={false} enableResetButton={false} - getRowHeight={getRowHeight} - onColumnResized={onColumnResized} /> From 9bdace3cc3a1677c1ed8e622f912a151bbc6dcb7 Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 3 Dec 2024 04:15:33 -0800 Subject: [PATCH 05/38] . --- frontend/src/utils/grid/cellRenderers.jsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index aaea3d5dd..0e7d1cf46 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -7,6 +7,7 @@ import { } from '@/constants/statuses' import { Link, useLocation } from 'react-router-dom' import { useState, useRef, useEffect } from 'react' +import colors from '@/themes/base/colors' export const TextRenderer = (props) => { return ( @@ -318,13 +319,13 @@ const GenericChipRenderer = ({ for (let i = 0; i < options.length; i++) { const chipText = options[i] const chipTextWidth = chipText.length * 6 // Assuming 6 pixels per character - const newTotalWidth = totalWidth + chipTextWidth + 32 // Add 32px for padding + const newTotalWidth = totalWidth + chipTextWidth + 32 + 12 // Add 32px for padding and 12px for overflow counter chip if (newTotalWidth <= containerWidth) { chipWidths.push({ text: chipText, width: chipTextWidth + 32, ...chipConfig }) totalWidth = newTotalWidth } else { - // Stop and calculate remaining chips + // calculate remaining chips setVisibleChips(chipWidths) setHiddenChipsCount(options.length - chipWidths.length) return @@ -376,7 +377,7 @@ const defaultRenderChip = (chip) => ( justifyContent: 'center', lineHeight: '23px', padding: '0.5rem', - backgroundColor: '#606060', + backgroundColor: `${colors.input.main}`, color: '#fff', margin: '0 2px', width: `${chip.width}px`, @@ -402,7 +403,7 @@ const defaultRenderOverflowChip = (hiddenChipsCount) => backgroundColor: 'rgba(0, 0, 0, 0.08)', borderRadius: '16px', padding: '0.5rem', - color: 'rgb(49, 49, 50)', + color: `${colors.text.main}`, cursor: 'text', margin: '0 2px', fontSize: '0.8125rem', @@ -450,7 +451,7 @@ const roleRenderOverflowChip = (hiddenChipsCount, isGovernmentRole = false) => backgroundColor: isGovernmentRole ? 'rgba(0, 51, 102, 0.3)' : 'rgba(252, 186, 25, 0.3)', borderRadius: '16px', padding: '0.5rem', - color: 'rgb(49, 49, 50)', + color: `${colors.text.main}`, cursor: 'text', margin: '0 2px', fontSize: '0.8125rem', From 5e01d56cbae4308f6e24aba46ab981e90506d0e8 Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 3 Dec 2024 04:34:28 -0800 Subject: [PATCH 06/38] . --- frontend/src/utils/grid/cellRenderers.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index 0e7d1cf46..e8ed5381d 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -319,7 +319,7 @@ const GenericChipRenderer = ({ for (let i = 0; i < options.length; i++) { const chipText = options[i] const chipTextWidth = chipText.length * 6 // Assuming 6 pixels per character - const newTotalWidth = totalWidth + chipTextWidth + 32 + 12 // Add 32px for padding and 12px for overflow counter chip + const newTotalWidth = totalWidth + chipTextWidth + 32 + 20 // Add 32px for padding and 20px for overflow counter chip if (newTotalWidth <= containerWidth) { chipWidths.push({ text: chipText, width: chipTextWidth + 32, ...chipConfig }) From ebd3b6fd832d9d5305870a99cb164e5c89de480f Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 3 Dec 2024 06:23:41 -0800 Subject: [PATCH 07/38] fix re-rendering issue --- frontend/src/utils/grid/cellRenderers.jsx | 109 +++++++++++++++++----- 1 file changed, 84 insertions(+), 25 deletions(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index e8ed5381d..91b1108aa 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -6,7 +6,7 @@ import { getAllOrganizationStatuses } from '@/constants/statuses' import { Link, useLocation } from 'react-router-dom' -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useMemo, useCallback } from 'react' import colors from '@/themes/base/colors' export const TextRenderer = (props) => { @@ -298,46 +298,100 @@ const GenericChipRenderer = ({ disableLink = false, renderChip = defaultRenderChip, renderOverflowChip = defaultRenderOverflowChip, - chipConfig = {} + chipConfig = {}, + ...props }) => { const location = useLocation() + const { colDef, api } = props const containerRef = useRef(null) const [visibleChips, setVisibleChips] = useState([]) const [hiddenChipsCount, setHiddenChipsCount] = useState(0) const options = Array.isArray(value) ? value - : value.split(',').map(item => item.trim()).filter(Boolean) + : value + .split(',') + .map((item) => item.trim()) + .filter(Boolean) - useEffect(() => { - if (!containerRef.current) return + const calculateChipWidths = useCallback(() => { + if (!containerRef.current) return { visibleChips: [], hiddenChipsCount: 0 } - const containerWidth = containerRef.current.offsetWidth + const containerWidth = containerRef.current.offsetWidth || 200 // Fallback width let totalWidth = 0 const chipWidths = [] for (let i = 0; i < options.length; i++) { const chipText = options[i] - const chipTextWidth = chipText.length * 6 // Assuming 6 pixels per character - const newTotalWidth = totalWidth + chipTextWidth + 32 + 20 // Add 32px for padding and 20px for overflow counter chip + const chipTextWidth = chipText.length * 6 // Assuming 6px per character + const newTotalWidth = totalWidth + chipTextWidth + 32 + 20 // Adding 32px for padding and 20px for overflow counter chip if (newTotalWidth <= containerWidth) { - chipWidths.push({ text: chipText, width: chipTextWidth + 32, ...chipConfig }) + chipWidths.push({ + text: chipText, + width: chipTextWidth + 32, + ...chipConfig + }) totalWidth = newTotalWidth } else { - // calculate remaining chips - setVisibleChips(chipWidths) - setHiddenChipsCount(options.length - chipWidths.length) - return + return { + visibleChips: chipWidths, + hiddenChipsCount: options.length - chipWidths.length + } } } - // If all chips fit - setVisibleChips( - options.map((text) => ({ text, width: text.length * 6 + 32, ...chipConfig })) - ) - setHiddenChipsCount(0) - }, [chipConfig, options]) + return { + visibleChips: options.map((text) => ({ + text, + width: text.length * 6 + 32, + ...chipConfig + })), + hiddenChipsCount: 0 + } + }, [options]) + + // Initial render and resize handling + useEffect(() => { + // Calculate and set chips on initial render + const { visibleChips, hiddenChipsCount } = calculateChipWidths() + setVisibleChips(visibleChips) + setHiddenChipsCount(hiddenChipsCount) + + // Resize listener + const resizeObserver = new ResizeObserver(() => { + const { visibleChips, hiddenChipsCount } = calculateChipWidths() + setVisibleChips(visibleChips) + setHiddenChipsCount(hiddenChipsCount) + }) + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + } + + // Column resize listener for ag-Grid + const resizeListener = (event) => { + const resizedColumn = event.column + if (resizedColumn.getColId() === colDef.field) { + const { visibleChips, hiddenChipsCount } = calculateChipWidths() + setVisibleChips(visibleChips) + setHiddenChipsCount(hiddenChipsCount) + } + } + + if (api) { + api.addEventListener('columnResized', resizeListener) + + // Cleanup + return () => { + api.removeEventListener('columnResized', resizeListener) + resizeObserver.disconnect() + } + } + + return () => { + resizeObserver.disconnect() + } + }, [value, api, colDef]) const chipContent = (
{ const filteredRoles = Array.isArray(value) ? value - : value.split(',').map((role) => role.trim()).filter( - role => role !== roles.government && role !== roles.supplier - ) + : value + .split(',') + .map((role) => role.trim()) + .filter((role) => role !== roles.government && role !== roles.supplier) useEffect(() => { setIsGovernmentRole( @@ -505,7 +562,9 @@ export const RoleRenderer = (props) => { {...props} value={filteredRoles} renderChip={(chip) => roleRenderChip(chip, isGovernmentRole)} - renderOverflowChip={(count) => roleRenderOverflowChip(count, isGovernmentRole)} + renderOverflowChip={(count) => + roleRenderOverflowChip(count, isGovernmentRole) + } /> ) -} \ No newline at end of file +} From b89fae97eeb15a5bc1802e3a582a2f951b3ce271 Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 3 Dec 2024 06:25:01 -0800 Subject: [PATCH 08/38] . --- frontend/src/utils/grid/cellRenderers.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index 91b1108aa..81a2d5f6b 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -529,7 +529,6 @@ const roleRenderOverflowChip = (hiddenChipsCount, isGovernmentRole = false) => export default GenericChipRenderer -// Updated CommonArrayRenderer export const CommonArrayRenderer = (props) => ( Date: Tue, 3 Dec 2024 06:25:01 -0800 Subject: [PATCH 09/38] . --- frontend/src/utils/grid/cellRenderers.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index 81a2d5f6b..2b0a7c665 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -310,9 +310,9 @@ const GenericChipRenderer = ({ const options = Array.isArray(value) ? value : value - .split(',') - .map((item) => item.trim()) - .filter(Boolean) + .split(',') + .map((item) => item.trim()) + .filter(Boolean) const calculateChipWidths = useCallback(() => { if (!containerRef.current) return { visibleChips: [], hiddenChipsCount: 0 } @@ -544,9 +544,9 @@ export const RoleRenderer = (props) => { const filteredRoles = Array.isArray(value) ? value : value - .split(',') - .map((role) => role.trim()) - .filter((role) => role !== roles.government && role !== roles.supplier) + .split(',') + .map((role) => role.trim()) + .filter((role) => role !== roles.government && role !== roles.supplier) useEffect(() => { setIsGovernmentRole( From 18e168e6686e8962270e6368793e90c37b2c79ba Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 3 Dec 2024 06:28:20 -0800 Subject: [PATCH 10/38] code formatting --- frontend/src/utils/grid/cellRenderers.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index 2b0a7c665..dc2a5e0e7 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import BCBadge from '@/components/BCBadge' import BCBox from '@/components/BCBox' import { roles } from '@/constants/roles' @@ -6,7 +7,7 @@ import { getAllOrganizationStatuses } from '@/constants/statuses' import { Link, useLocation } from 'react-router-dom' -import { useState, useRef, useEffect, useMemo, useCallback } from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' import colors from '@/themes/base/colors' export const TextRenderer = (props) => { @@ -36,7 +37,6 @@ export const LinkRenderer = (props) => { } export const StatusRenderer = (props) => { - const location = useLocation() return ( Date: Tue, 3 Dec 2024 09:16:51 -0800 Subject: [PATCH 11/38] feat: first round seeder changes --- .../db/seeders/common/seed_fuel_data.json | 174 +++++++++++++++++- 1 file changed, 172 insertions(+), 2 deletions(-) diff --git a/backend/lcfs/db/seeders/common/seed_fuel_data.json b/backend/lcfs/db/seeders/common/seed_fuel_data.json index 9f29e04b5..fb71fc4f4 100644 --- a/backend/lcfs/db/seeders/common/seed_fuel_data.json +++ b/backend/lcfs/db/seeders/common/seed_fuel_data.json @@ -97,7 +97,6 @@ "units": "kg", "unrecognized": false }, - { "fuel_type_id": 11, "fuel_type": "Alternative jet fuel", @@ -365,6 +364,51 @@ "end_use_type_id": 21, "type": "Aircraft", "intended_use": true + }, + { + "end_use_type_id": 22, + "type": "Compression-ignition engine- Marine, general", + "intended_use": true + }, + { + "end_use_type_id": 23, + "type": "Compression-ignition engine- Marine, operated within 51 to 75% of load range", + "intended_use": true + }, + { + "end_use_type_id": 24, + "type": "Compression-ignition engine- Marine, operated within 76 to 100% of load range", + "intended_use": true + }, + { + "end_use_type_id": 25, + "type": "Compression-ignition engine- Marine, with methane slip reduction kit- General", + "intended_use": true + }, + { + "end_use_type_id": 26, + "type": "Compression-ignition engine- Marine, with methane slip reduction kit- Operated within 51 to 75% of load range", + "intended_use": true + }, + { + "end_use_type_id": 27, + "type": "Compression-ignition engine- Marine, with methane slip reduction kit- Operated within 76 to 100% of load range", + "intended_use": true + }, + { + "end_use_type_id": 28, + "type": "Compression-ignition engine- Marine, unknown whether kit is installed or average operating load range", + "intended_use": true + }, + { + "end_use_type_id": 29, + "type": "Unknown engine type", + "intended_use": true + }, + { + "end_use_type_id": 30, + "type": "Other (i.e. road transportation)", + "intended_use": true } ], "unit_of_measures": [ @@ -454,6 +498,69 @@ "additional_uci_id": 9, "uom_id": 5, "intensity": 0 + }, + { + "additional_uci_id": 10, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 22, + "intensity": 27.3 + }, + { + "additional_uci_id": 11, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 23, + "intensity": 17.8 + }, + { + "additional_uci_id": 12, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 24, + "intensity": 12.2 + }, + { + "additional_uci_id": 13, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 25, + "intensity": 10.6 + }, + { + "additional_uci_id": 14, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 26, + "intensity": 8.4 + }, + { + "additional_uci_id": 15, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 27, + "intensity": 8.0 + }, + { + "additional_uci_id": 16, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 28, + "intensity": 27.3 + }, + { + "additional_uci_id": 17, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 29, + "intensity": 27.3 + }, + { + "additional_uci_id": 18, + "fuel_type_id": 7, + "uom_id": 5, + "end_use_type_id": 30, + "intensity": 0 } ], "eers": [ @@ -618,6 +725,69 @@ "fuel_category_id": 3, "fuel_type_id": 11, "ratio": 1.0 + }, + { + "eer_id": 25, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 22, + "ratio": 1.0 + }, + { + "eer_id": 26, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 23, + "ratio": 1.0 + }, + { + "eer_id": 27, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 24, + "ratio": 1.0 + }, + { + "eer_id": 28, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 25, + "ratio": 1.0 + }, + { + "eer_id": 29, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 26, + "ratio": 1.0 + }, + { + "eer_id": 30, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 27, + "ratio": 1.0 + }, + { + "eer_id": 31, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 28, + "ratio": 1.0 + }, + { + "eer_id": 32, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 29, + "ratio": 0.9 + }, + { + "eer_id": 33, + "fuel_category_id": 2, + "fuel_type_id": 7, + "end_use_type_id": 30, + "ratio": 0.9 } ], "energy_densities": [ @@ -1027,4 +1197,4 @@ "display_order": 4 } ] -} +} \ No newline at end of file From 005ef68c35bc3348550ff34b0bce16f3c12ecec4 Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Tue, 3 Dec 2024 09:43:18 -0800 Subject: [PATCH 12/38] chore: clean up IDs --- .../db/seeders/common/seed_fuel_data.json | 207 +++++------------- 1 file changed, 51 insertions(+), 156 deletions(-) diff --git a/backend/lcfs/db/seeders/common/seed_fuel_data.json b/backend/lcfs/db/seeders/common/seed_fuel_data.json index fb71fc4f4..a3e7cde3a 100644 --- a/backend/lcfs/db/seeders/common/seed_fuel_data.json +++ b/backend/lcfs/db/seeders/common/seed_fuel_data.json @@ -327,86 +327,51 @@ }, { "end_use_type_id": 14, - "type": "Marine", - "sub_type": "General", - "intended_use": true - }, - { - "end_use_type_id": 15, - "type": "Marine", - "sub_type": "Operated within 51 to 75% of load range" - }, - { - "end_use_type_id": 16, - "type": "Marine", - "sub_type": "Operated within 76 to 100% of load range" - }, - { - "end_use_type_id": 17, - "type": "Marine, w/ methane slip reduction kit", - "sub_type": "General" - }, - { - "end_use_type_id": 18, - "type": "Marine, w/ methane slip reduction kit", - "sub_type": "Operated within 51 to 75% of load range" - }, - { - "end_use_type_id": 19, - "type": "Marine, w/ methane slip reduction kit", - "sub_type": "Operated within 76 to 100% of load range" - }, - { - "end_use_type_id": 20, - "type": "Unknown" - }, - { - "end_use_type_id": 21, "type": "Aircraft", "intended_use": true }, { - "end_use_type_id": 22, + "end_use_type_id": 15, "type": "Compression-ignition engine- Marine, general", "intended_use": true }, { - "end_use_type_id": 23, + "end_use_type_id": 16, "type": "Compression-ignition engine- Marine, operated within 51 to 75% of load range", "intended_use": true }, { - "end_use_type_id": 24, + "end_use_type_id": 17, "type": "Compression-ignition engine- Marine, operated within 76 to 100% of load range", "intended_use": true }, { - "end_use_type_id": 25, + "end_use_type_id": 18, "type": "Compression-ignition engine- Marine, with methane slip reduction kit- General", "intended_use": true }, { - "end_use_type_id": 26, + "end_use_type_id": 19, "type": "Compression-ignition engine- Marine, with methane slip reduction kit- Operated within 51 to 75% of load range", "intended_use": true }, { - "end_use_type_id": 27, + "end_use_type_id": 20, "type": "Compression-ignition engine- Marine, with methane slip reduction kit- Operated within 76 to 100% of load range", "intended_use": true }, { - "end_use_type_id": 28, + "end_use_type_id": 21, "type": "Compression-ignition engine- Marine, unknown whether kit is installed or average operating load range", "intended_use": true }, { - "end_use_type_id": 29, + "end_use_type_id": 22, "type": "Unknown engine type", "intended_use": true }, { - "end_use_type_id": 30, + "end_use_type_id": 23, "type": "Other (i.e. road transportation)", "intended_use": true } @@ -443,61 +408,61 @@ "additional_uci_id": 1, "fuel_type_id": 7, "uom_id": 5, - "end_use_type_id": 14, - "intensity": 27.3 + "intensity": 0 }, { "additional_uci_id": 2, - "fuel_type_id": 7, "uom_id": 5, - "end_use_type_id": 15, - "intensity": 17.8 + "intensity": 0 }, { "additional_uci_id": 3, "fuel_type_id": 7, "uom_id": 5, - "end_use_type_id": 16, - "intensity": 12.2 + "end_use_type_id": 15, + "intensity": 27.3 }, { "additional_uci_id": 4, "fuel_type_id": 7, "uom_id": 5, - "end_use_type_id": 17, - "intensity": 10.6 + "end_use_type_id": 16, + "intensity": 17.8 }, { "additional_uci_id": 5, "fuel_type_id": 7, "uom_id": 5, - "end_use_type_id": 18, - "intensity": 8.4 + "end_use_type_id": 17, + "intensity": 12.2 }, { "additional_uci_id": 6, "fuel_type_id": 7, "uom_id": 5, - "end_use_type_id": 19, - "intensity": 8.0 + "end_use_type_id": 18, + "intensity": 10.6 }, { "additional_uci_id": 7, "fuel_type_id": 7, "uom_id": 5, - "end_use_type_id": 20, - "intensity": 27.3 + "end_use_type_id": 19, + "intensity": 8.4 }, { "additional_uci_id": 8, "fuel_type_id": 7, "uom_id": 5, - "intensity": 0 + "end_use_type_id": 20, + "intensity": 8.0 }, { "additional_uci_id": 9, + "fuel_type_id": 7, "uom_id": 5, - "intensity": 0 + "end_use_type_id": 21, + "intensity": 27.3 }, { "additional_uci_id": 10, @@ -511,55 +476,6 @@ "fuel_type_id": 7, "uom_id": 5, "end_use_type_id": 23, - "intensity": 17.8 - }, - { - "additional_uci_id": 12, - "fuel_type_id": 7, - "uom_id": 5, - "end_use_type_id": 24, - "intensity": 12.2 - }, - { - "additional_uci_id": 13, - "fuel_type_id": 7, - "uom_id": 5, - "end_use_type_id": 25, - "intensity": 10.6 - }, - { - "additional_uci_id": 14, - "fuel_type_id": 7, - "uom_id": 5, - "end_use_type_id": 26, - "intensity": 8.4 - }, - { - "additional_uci_id": 15, - "fuel_type_id": 7, - "uom_id": 5, - "end_use_type_id": 27, - "intensity": 8.0 - }, - { - "additional_uci_id": 16, - "fuel_type_id": 7, - "uom_id": 5, - "end_use_type_id": 28, - "intensity": 27.3 - }, - { - "additional_uci_id": 17, - "fuel_type_id": 7, - "uom_id": 5, - "end_use_type_id": 29, - "intensity": 27.3 - }, - { - "additional_uci_id": 18, - "fuel_type_id": 7, - "uom_id": 5, - "end_use_type_id": 30, "intensity": 0 } ], @@ -656,137 +572,116 @@ "eer_id": 14, "fuel_category_id": 2, "fuel_type_id": 3, - "end_use_type_id": 14, - "ratio": 2.5 - }, - { - "eer_id": 15, - "fuel_category_id": 2, - "fuel_type_id": 3, "end_use_type_id": 10, "ratio": 2.8 }, { - "eer_id": 16, + "eer_id": 15, "fuel_category_id": 2, "fuel_type_id": 3, "end_use_type_id": 11, "ratio": 2.4 }, { - "eer_id": 17, + "eer_id": 16, "fuel_category_id": 2, "fuel_type_id": 3, "end_use_type_id": 2, "ratio": 1.0 }, { - "eer_id": 18, + "eer_id": 17, "fuel_category_id": 2, "fuel_type_id": 6, "end_use_type_id": 3, "ratio": 1.8 }, { - "eer_id": 19, + "eer_id": 18, "fuel_category_id": 2, "fuel_type_id": 6, "end_use_type_id": 2, "ratio": 0.9 }, { - "eer_id": 20, - "fuel_category_id": 2, - "fuel_type_id": 7, - "end_use_type_id": 12, - "ratio": 1.0 - }, - { - "eer_id": 21, - "fuel_category_id": 2, - "fuel_type_id": 7, - "end_use_type_id": 2, - "ratio": 0.9 - }, - { - "eer_id": 22, + "eer_id": 19, "fuel_category_id": 2, "fuel_type_id": 13, "ratio": 0.9 }, { - "eer_id": 23, + "eer_id": 20, "fuel_category_id": 3, "fuel_type_id": 3, "ratio": 2.5 }, { - "eer_id": 24, + "eer_id": 21, "fuel_category_id": 3, "fuel_type_id": 11, "ratio": 1.0 }, { - "eer_id": 25, + "eer_id": 22, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 22, + "end_use_type_id": 15, "ratio": 1.0 }, { - "eer_id": 26, + "eer_id": 23, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 23, + "end_use_type_id": 16, "ratio": 1.0 }, { - "eer_id": 27, + "eer_id": 24, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 24, + "end_use_type_id": 17, "ratio": 1.0 }, { - "eer_id": 28, + "eer_id": 25, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 25, + "end_use_type_id": 18, "ratio": 1.0 }, { - "eer_id": 29, + "eer_id": 26, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 26, + "end_use_type_id": 19, "ratio": 1.0 }, { - "eer_id": 30, + "eer_id": 27, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 27, + "end_use_type_id": 20, "ratio": 1.0 }, { - "eer_id": 31, + "eer_id": 28, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 28, + "end_use_type_id": 21, "ratio": 1.0 }, { - "eer_id": 32, + "eer_id": 29, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 29, + "end_use_type_id": 22, "ratio": 0.9 }, { - "eer_id": 33, + "eer_id": 30, "fuel_category_id": 2, "fuel_type_id": 7, - "end_use_type_id": 30, + "end_use_type_id": 23, "ratio": 0.9 } ], From dca410877dff97a02a6f7bd0278a3c63566d23ec Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Tue, 3 Dec 2024 09:46:16 -0800 Subject: [PATCH 13/38] fix: end use col width --- frontend/src/views/FuelSupplies/_schema.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/FuelSupplies/_schema.jsx b/frontend/src/views/FuelSupplies/_schema.jsx index 4984dc813..7273986b8 100644 --- a/frontend/src/views/FuelSupplies/_schema.jsx +++ b/frontend/src/views/FuelSupplies/_schema.jsx @@ -251,7 +251,7 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ } return true }, - minWidth: 260 + minWidth: 400 }, { field: 'provisionOfTheAct', From 9abdf7db1ff04f11ba50f59fd7afea8177be8b0a Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Tue, 3 Dec 2024 09:50:22 -0800 Subject: [PATCH 14/38] fix: col width for exports --- frontend/src/views/FuelExports/_schema.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/FuelExports/_schema.jsx b/frontend/src/views/FuelExports/_schema.jsx index 3be0ee533..8478ed0cd 100644 --- a/frontend/src/views/FuelExports/_schema.jsx +++ b/frontend/src/views/FuelExports/_schema.jsx @@ -279,7 +279,7 @@ export const fuelExportColDefs = (optionsData, errors) => [ } return true }, - minWidth: 260 + minWidth: 400 }, { field: 'provisionOfTheAct', From 35cad8e533a63d984f1cfa03519b1fdbb891908f Mon Sep 17 00:00:00 2001 From: Hamed Valiollahi Bayeki Date: Tue, 3 Dec 2024 11:22:07 -0800 Subject: [PATCH 15/38] feat: prevent copying 'kWh usage' field in duplicate row function --- .../web/api/final_supply_equipment/schema.py | 2 +- .../AddEditFinalSupplyEquipments.jsx | 31 ++++++++++++------- .../views/FinalSupplyEquipments/_schema.jsx | 27 ++++++++-------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/backend/lcfs/web/api/final_supply_equipment/schema.py b/backend/lcfs/web/api/final_supply_equipment/schema.py index 0751354bd..38f80b2fa 100644 --- a/backend/lcfs/web/api/final_supply_equipment/schema.py +++ b/backend/lcfs/web/api/final_supply_equipment/schema.py @@ -40,7 +40,7 @@ class FinalSupplyEquipmentCreateSchema(BaseSchema): compliance_report_id: Optional[int] = None supply_from_date: date supply_to_date: date - kwh_usage: Optional[float] = None + kwh_usage: float serial_nbr: str manufacturer: str model: Optional[str] = None diff --git a/frontend/src/views/FinalSupplyEquipments/AddEditFinalSupplyEquipments.jsx b/frontend/src/views/FinalSupplyEquipments/AddEditFinalSupplyEquipments.jsx index 5fb458294..fbfa3ceb3 100644 --- a/frontend/src/views/FinalSupplyEquipments/AddEditFinalSupplyEquipments.jsx +++ b/frontend/src/views/FinalSupplyEquipments/AddEditFinalSupplyEquipments.jsx @@ -151,8 +151,9 @@ export const AddEditFinalSupplyEquipments = () => { if (fields[0] === 'postalCode') { errMsg = t('finalSupplyEquipment:postalCodeError') } else { - errMsg = `Error updating row: ${fieldLabels.length === 1 ? fieldLabels[0] : '' - } ${String(message).toLowerCase()}` + errMsg = `Error updating row: ${ + fieldLabels.length === 1 ? fieldLabels[0] : '' + } ${String(message).toLowerCase()}` } } else { errMsg = error.response.data?.detail @@ -206,6 +207,7 @@ export const AddEditFinalSupplyEquipments = () => { const rowData = { ...params.node.data, id: newRowID, + kwhUsage: null, serialNbr: null, latitude: null, longitude: null, @@ -239,16 +241,21 @@ export const AddEditFinalSupplyEquipments = () => { ) }, [navigate, compliancePeriod, complianceReportId]) - const onAddRows = useCallback((numRows) => { - return Array(numRows).fill().map(() => ({ - id: uuid(), - complianceReportId, - supplyFromDate: `${compliancePeriod}-01-01`, - supplyToDate: `${compliancePeriod}-12-31`, - validationStatus: 'error', - modified: true - })) - }, [compliancePeriod, complianceReportId]) + const onAddRows = useCallback( + (numRows) => { + return Array(numRows) + .fill() + .map(() => ({ + id: uuid(), + complianceReportId, + supplyFromDate: `${compliancePeriod}-01-01`, + supplyToDate: `${compliancePeriod}-12-31`, + validationStatus: 'error', + modified: true + })) + }, + [compliancePeriod, complianceReportId] + ) return ( isFetched && diff --git a/frontend/src/views/FinalSupplyEquipments/_schema.jsx b/frontend/src/views/FinalSupplyEquipments/_schema.jsx index fb431246e..67a25c44d 100644 --- a/frontend/src/views/FinalSupplyEquipments/_schema.jsx +++ b/frontend/src/views/FinalSupplyEquipments/_schema.jsx @@ -14,11 +14,7 @@ import { CommonArrayRenderer } from '@/utils/grid/cellRenderers' import { StandardCellErrors } from '@/utils/grid/errorRenderers' import { apiRoutes } from '@/constants/routes' -export const finalSupplyEquipmentColDefs = ( - optionsData, - compliancePeriod, - errors -) => [ +export const finalSupplyEquipmentColDefs = (optionsData, compliancePeriod, errors) => [ validation, actions({ enableDuplicate: true, @@ -75,6 +71,7 @@ export const finalSupplyEquipmentColDefs = ( }, { field: 'kwhUsage', + headerComponent: RequiredHeader, headerName: i18n.t( 'finalSupplyEquipment:finalSupplyEquipmentColLabels.kwhUsage' ), @@ -83,9 +80,9 @@ export const finalSupplyEquipmentColDefs = ( cellDataType: 'text', cellStyle: (params) => StandardCellErrors(params, errors), valueFormatter: (params) => { - const value = parseFloat(params.value); - return !isNaN(value) ? value.toFixed(2) : ''; - }, + const value = parseFloat(params.value) + return !isNaN(value) ? value.toFixed(2) : '' + } }, { field: 'serialNbr', @@ -110,13 +107,15 @@ export const finalSupplyEquipmentColDefs = ( queryKey: 'fuel-code-search', queryFn: async ({ client, queryKey }) => { try { - const [, searchTerm] = queryKey; - const path = `${apiRoutes.searchFinalSupplyEquipments}manufacturer=${encodeURIComponent(searchTerm)}`; - const response = await client.get(path); - return response.data; + const [, searchTerm] = queryKey + const path = `${ + apiRoutes.searchFinalSupplyEquipments + }manufacturer=${encodeURIComponent(searchTerm)}` + const response = await client.get(path) + return response.data } catch (error) { - console.error('Error fetching manufacturer data:', error); - return []; + console.error('Error fetching manufacturer data:', error) + return [] } }, optionLabel: 'manufacturer', From 6aa450d2bd613e637da4ae9f293be1896c0afad3 Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Tue, 3 Dec 2024 12:24:10 -0800 Subject: [PATCH 16/38] fix: bceid chained reports --- backend/lcfs/web/api/compliance_report/services.py | 8 ++++++-- backend/lcfs/web/api/organization/views.py | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/backend/lcfs/web/api/compliance_report/services.py b/backend/lcfs/web/api/compliance_report/services.py index a1bc239f7..6d562aaf8 100644 --- a/backend/lcfs/web/api/compliance_report/services.py +++ b/backend/lcfs/web/api/compliance_report/services.py @@ -52,7 +52,8 @@ async def create_compliance_report( report_data.status ) if not draft_status: - raise DataNotFoundException(f"Status '{report_data.status}' not found.") + raise DataNotFoundException( + f"Status '{report_data.status}' not found.") # Generate a new group_uuid for the new report series group_uuid = str(uuid.uuid4()) @@ -193,6 +194,7 @@ def _mask_report_status(self, reports: List) -> List: ComplianceReportStatusEnum.Submitted.value ) report.current_status.compliance_report_status_id = None + masked_reports.append(report) else: masked_reports.append(report) @@ -223,10 +225,12 @@ async def get_compliance_report_by_id( compliance_report_chain = await self.repo.get_compliance_report_chain( report.compliance_report_group_uuid ) + masked_compliance_report_chain = (self._mask_report_status( + compliance_report_chain) if apply_masking else compliance_report_chain) return { "report": history_masked_report, - "chain": compliance_report_chain, + "chain": masked_compliance_report_chain, } return history_masked_report diff --git a/backend/lcfs/web/api/organization/views.py b/backend/lcfs/web/api/organization/views.py index eb6244051..e175bf756 100644 --- a/backend/lcfs/web/api/organization/views.py +++ b/backend/lcfs/web/api/organization/views.py @@ -33,6 +33,7 @@ ComplianceReportCreateSchema, ComplianceReportListSchema, CompliancePeriodSchema, + ChainedComplianceReportSchema ) from lcfs.web.api.compliance_report.services import ComplianceReportServices from .services import OrganizationService @@ -55,7 +56,8 @@ async def get_org_users( request: Request, organization_id: int, - status: str = Query(default="Active", description="Active or Inactive users list"), + status: str = Query( + default="Active", description="Active or Inactive users list"), pagination: PaginationRequestSchema = Body(..., embed=False), response: Response = None, org_service: OrganizationService = Depends(), @@ -264,7 +266,7 @@ async def get_compliance_reports( ) -> ComplianceReportListSchema: organization_id = request.user.organization.organization_id return await report_service.get_compliance_reports_paginated( - pagination, organization_id, bceid_user = True + pagination, organization_id, bceid_user=True ) @@ -288,7 +290,7 @@ async def get_all_org_reported_years( @router.get( "/{organization_id}/reports/{report_id}", - response_model=ComplianceReportBaseSchema, + response_model=ChainedComplianceReportSchema, status_code=status.HTTP_200_OK, ) @view_handler([RoleEnum.SUPPLIER]) @@ -299,10 +301,10 @@ async def get_compliance_report_by_id( report_id: int = None, report_service: ComplianceReportServices = Depends(), report_validate: ComplianceReportValidation = Depends(), -) -> ComplianceReportBaseSchema: +) -> ChainedComplianceReportSchema: """ Endpoint to get information of a user by ID This endpoint returns the information of a user by ID, including their roles and organization. """ await report_validate.validate_organization_access(report_id) - return await report_service.get_compliance_report_by_id(report_id, apply_masking=True) + return await report_service.get_compliance_report_by_id(report_id, apply_masking=True, get_chain=True) From 96d0b90fea95cfa15ac743f3bee12ddd0b1582e1 Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 3 Dec 2024 13:07:54 -0800 Subject: [PATCH 17/38] fix: updated dependency injections for services, fixed connection pool issue --- backend/lcfs/dependencies/dependencies.py | 9 ------ .../services/rabbitmq/transaction_consumer.py | 6 ++-- backend/lcfs/services/redis/dependency.py | 29 ++++++++---------- backend/lcfs/services/redis/lifetime.py | 16 ++++++---- backend/lcfs/services/s3/client.py | 6 ++-- backend/lcfs/services/s3/dependency.py | 19 ++++++++++++ backend/lcfs/services/s3/lifetime.py | 30 +++++++++++++++++++ backend/lcfs/web/lifetime.py | 30 ++----------------- 8 files changed, 80 insertions(+), 65 deletions(-) delete mode 100644 backend/lcfs/dependencies/dependencies.py create mode 100644 backend/lcfs/services/s3/dependency.py create mode 100644 backend/lcfs/services/s3/lifetime.py diff --git a/backend/lcfs/dependencies/dependencies.py b/backend/lcfs/dependencies/dependencies.py deleted file mode 100644 index 9d160d0dc..000000000 --- a/backend/lcfs/dependencies/dependencies.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import Request -from redis.asyncio import Redis -import boto3 - -async def get_redis_pool(request: Request) -> Redis: - return request.app.state.redis_pool - -async def get_s3_client(request: Request) -> boto3.client: - return request.app.state.s3_client \ No newline at end of file diff --git a/backend/lcfs/services/rabbitmq/transaction_consumer.py b/backend/lcfs/services/rabbitmq/transaction_consumer.py index 10e5367f4..09626551a 100644 --- a/backend/lcfs/services/rabbitmq/transaction_consumer.py +++ b/backend/lcfs/services/rabbitmq/transaction_consumer.py @@ -4,7 +4,7 @@ from redis.asyncio import Redis from sqlalchemy.ext.asyncio import AsyncSession -from lcfs.dependencies.dependencies import get_redis_pool +from lcfs.services.redis.dependency import get_redis_pool from fastapi import Request from lcfs.db.dependencies import async_engine @@ -50,14 +50,14 @@ async def process_message(self, body: bytes, request: Request): compliance_units = message_content.get("compliance_units_amount") org_id = message_content.get("organization_id") - redis = await get_redis_pool(request) + redis_pool = await get_redis_pool(request) async with AsyncSession(async_engine) as session: async with session.begin(): repo = OrganizationsRepository(db=session) transaction_repo = TransactionRepository(db=session) redis_balance_service = RedisBalanceService( - transaction_repo=transaction_repo, redis_pool=redis.connection_pool + transaction_repo=transaction_repo, redis_pool=redis_pool ) org_service = OrganizationsService( repo=repo, diff --git a/backend/lcfs/services/redis/dependency.py b/backend/lcfs/services/redis/dependency.py index 368994ffd..14a19190c 100644 --- a/backend/lcfs/services/redis/dependency.py +++ b/backend/lcfs/services/redis/dependency.py @@ -1,26 +1,23 @@ -from typing import AsyncGenerator - -from redis.asyncio import Redis +from redis.asyncio import ConnectionPool from starlette.requests import Request +# Redis Pool Dependency async def get_redis_pool( request: Request, -) -> AsyncGenerator[Redis, None]: # pragma: no cover +) -> ConnectionPool: """ - Returns connection pool. - - You can use it like this: - - >>> from redis.asyncio import ConnectionPool, Redis - >>> - >>> async def handler(redis_pool: ConnectionPool = Depends(get_redis_pool)): - >>> async with Redis(connection_pool=redis_pool) as redis: - >>> await redis.get('key') + Returns the Redis connection pool. - I use pools, so you don't acquire connection till the end of the handler. + Usage: + >>> from redis.asyncio import ConnectionPool, Redis + >>> + >>> async def handler(redis_pool: ConnectionPool = Depends(get_redis_pool)): + >>> redis = Redis(connection_pool=redis_pool) + >>> await redis.get('key') + >>> await redis.close() - :param request: current request. - :returns: redis connection pool. + :param request: Current request object. + :returns: Redis connection pool. """ return request.app.state.redis_pool diff --git a/backend/lcfs/services/redis/lifetime.py b/backend/lcfs/services/redis/lifetime.py index 3959edbff..2e007adbd 100644 --- a/backend/lcfs/services/redis/lifetime.py +++ b/backend/lcfs/services/redis/lifetime.py @@ -1,12 +1,13 @@ import logging from fastapi import FastAPI -from redis import asyncio as aioredis +from redis.asyncio import ConnectionPool, Redis from redis.exceptions import RedisError from lcfs.settings import settings logger = logging.getLogger(__name__) + async def init_redis(app: FastAPI) -> None: """ Creates connection pool for redis. @@ -14,13 +15,16 @@ async def init_redis(app: FastAPI) -> None: :param app: current fastapi application. """ try: - app.state.redis_pool = aioredis.from_url( + app.state.redis_pool = ConnectionPool.from_url( str(settings.redis_url), encoding="utf8", decode_responses=True, - max_connections=200 + max_connections=200, ) - await app.state.redis_pool.ping() + # Test the connection + redis = Redis(connection_pool=app.state.redis_pool) + await redis.ping() + await redis.close() logger.info("Redis pool initialized successfully.") except RedisError as e: logger.error(f"Redis error during initialization: {e}") @@ -29,6 +33,7 @@ async def init_redis(app: FastAPI) -> None: logger.error(f"Unexpected error during Redis initialization: {e}") raise + async def shutdown_redis(app: FastAPI) -> None: # pragma: no cover """ Closes redis connection pool. @@ -37,8 +42,7 @@ async def shutdown_redis(app: FastAPI) -> None: # pragma: no cover """ try: if hasattr(app.state, "redis_pool"): - await app.state.redis_pool.close() - await app.state.redis_pool.wait_closed() + await app.state.redis_pool.disconnect(inuse_connections=True) logger.info("Redis pool closed successfully.") except RedisError as e: logger.error(f"Redis error during shutdown: {e}") diff --git a/backend/lcfs/services/s3/client.py b/backend/lcfs/services/s3/client.py index c03b54993..11ee27397 100644 --- a/backend/lcfs/services/s3/client.py +++ b/backend/lcfs/services/s3/client.py @@ -7,7 +7,7 @@ from sqlalchemy import select from sqlalchemy.exc import InvalidRequestError from sqlalchemy.ext.asyncio import AsyncSession -from lcfs.dependencies.dependencies import get_s3_client +from lcfs.services.s3.dependency import get_s3_client from lcfs.db.dependencies import get_async_db_session from lcfs.db.models.compliance import ComplianceReport @@ -28,13 +28,13 @@ class DocumentService: def __init__( self, - request: Request, db: AsyncSession = Depends(get_async_db_session), clamav_service: ClamAVService = Depends(), + s3_client=Depends(get_s3_client), ): self.db = db self.clamav_service = clamav_service - self.s3_client = request.app.state.s3_client + self.s3_client = s3_client @repo_handler async def upload_file(self, file, parent_id: str, parent_type="compliance_report"): diff --git a/backend/lcfs/services/s3/dependency.py b/backend/lcfs/services/s3/dependency.py new file mode 100644 index 000000000..d46027c42 --- /dev/null +++ b/backend/lcfs/services/s3/dependency.py @@ -0,0 +1,19 @@ +from starlette.requests import Request +import boto3 + + +# S3 Client Dependency +async def get_s3_client( + request: Request, +) -> boto3.client: + """ + Returns the S3 client from the application state. + + Usage: + >>> async def handler(s3_client = Depends(get_s3_client)): + >>> s3_client.upload_file('file.txt', 'my-bucket', 'file.txt') + + :param request: Current request object. + :returns: S3 client. + """ + return request.app.state.s3_client diff --git a/backend/lcfs/services/s3/lifetime.py b/backend/lcfs/services/s3/lifetime.py new file mode 100644 index 000000000..443f49eae --- /dev/null +++ b/backend/lcfs/services/s3/lifetime.py @@ -0,0 +1,30 @@ +import boto3 +from fastapi import FastAPI +from lcfs.settings import settings + + +async def init_s3(app: FastAPI) -> None: + """ + Initialize the S3 client and store it in the app state. + + :param app: FastAPI application. + """ + app.state.s3_client = boto3.client( + "s3", + aws_access_key_id=settings.s3_access_key, + aws_secret_access_key=settings.s3_secret_key, + endpoint_url=settings.s3_endpoint, + region_name="us-east-1", + ) + print("S3 client initialized.") + + +async def shutdown_s3(app: FastAPI) -> None: + """ + Cleanup the S3 client from the app state. + + :param app: FastAPI application. + """ + if hasattr(app.state, "s3_client"): + del app.state.s3_client + print("S3 client shutdown.") diff --git a/backend/lcfs/web/lifetime.py b/backend/lcfs/web/lifetime.py index 186b485cd..0215689e7 100644 --- a/backend/lcfs/web/lifetime.py +++ b/backend/lcfs/web/lifetime.py @@ -9,6 +9,7 @@ from lcfs.services.rabbitmq.consumers import start_consumers, stop_consumers from lcfs.services.redis.lifetime import init_redis, shutdown_redis +from lcfs.services.s3.lifetime import init_s3, shutdown_s3 from lcfs.services.tfrs.redis_balance import init_org_balance_cache from lcfs.settings import settings @@ -32,33 +33,6 @@ def _setup_db(app: FastAPI) -> None: # pragma: no cover app.state.db_session_factory = session_factory -async def startup_s3(app: FastAPI) -> None: - """ - Initialize the S3 client and store it in the app state. - - :param app: fastAPI application. - """ - app.state.s3_client = boto3.client( - "s3", - aws_access_key_id=settings.s3_access_key, - aws_secret_access_key=settings.s3_secret_key, - endpoint_url=settings.s3_endpoint, - region_name="us-east-1", - ) - print("S3 client initialized.") - - -async def shutdown_s3(app: FastAPI) -> None: - """ - Cleanup the S3 client from the app state. - - :param app: fastAPI application. - """ - if hasattr(app.state, "s3_client"): - del app.state.s3_client - print("S3 client shutdown.") - - def register_startup_event( app: FastAPI, ) -> Callable[[], Awaitable[None]]: # pragma: no cover @@ -89,7 +63,7 @@ async def _startup() -> None: # noqa: WPS430 await init_org_balance_cache(app) # Initialize the S3 client - await startup_s3(app) + await init_s3(app) # Setup RabbitMQ Listeners await start_consumers() From ddf19650836464a9f86d9006d643a320df5be36d Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Tue, 3 Dec 2024 13:10:32 -0800 Subject: [PATCH 18/38] fix: mask history --- .../lcfs/web/api/compliance_report/services.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/lcfs/web/api/compliance_report/services.py b/backend/lcfs/web/api/compliance_report/services.py index 6d562aaf8..31993bc75 100644 --- a/backend/lcfs/web/api/compliance_report/services.py +++ b/backend/lcfs/web/api/compliance_report/services.py @@ -225,12 +225,21 @@ async def get_compliance_report_by_id( compliance_report_chain = await self.repo.get_compliance_report_chain( report.compliance_report_group_uuid ) - masked_compliance_report_chain = (self._mask_report_status( - compliance_report_chain) if apply_masking else compliance_report_chain) + + if apply_masking: + # Apply masking to each report in the chain + masked_chain = self._mask_report_status( + compliance_report_chain) + # Apply history masking to each report in the chain + masked_chain = [ + self._mask_report_status_for_history(report, apply_masking) + for report in masked_chain + ] + compliance_report_chain = masked_chain return { "report": history_masked_report, - "chain": masked_compliance_report_chain, + "chain": compliance_report_chain, } return history_masked_report From 02c01cb16d82fd8fda85cdf7f7e08e7fc8b6ea90 Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Tue, 3 Dec 2024 13:32:43 -0800 Subject: [PATCH 19/38] fix: backend tests --- .../organization/test_organization_views.py | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/backend/lcfs/tests/organization/test_organization_views.py b/backend/lcfs/tests/organization/test_organization_views.py index a98a1a306..3f8cacafc 100644 --- a/backend/lcfs/tests/organization/test_organization_views.py +++ b/backend/lcfs/tests/organization/test_organization_views.py @@ -16,6 +16,7 @@ from lcfs.web.api.organization.validation import OrganizationValidation from lcfs.web.api.compliance_report.services import ComplianceReportServices +from lcfs.web.api.compliance_report.schema import ChainedComplianceReportSchema @pytest.mark.anyio @@ -160,7 +161,8 @@ async def test_export_transactions_for_org_success( ): set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) - mock_transactions_services.export_transactions.return_value = {"streaming": True} + mock_transactions_services.export_transactions.return_value = { + "streaming": True} fastapi_app.dependency_overrides[TransactionsService] = ( lambda: mock_transactions_services @@ -188,7 +190,8 @@ async def test_create_transfer_success( set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) organization_id = 1 - url = fastapi_app.url_path_for("create_transfer", organization_id=organization_id) + url = fastapi_app.url_path_for( + "create_transfer", organization_id=organization_id) payload = {"from_organization_id": 1, "to_organization_id": 2} @@ -226,7 +229,8 @@ async def test_update_transfer_success( ): set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) - url = fastapi_app.url_path_for("update_transfer", organization_id=1, transfer_id=1) + url = fastapi_app.url_path_for( + "update_transfer", organization_id=1, transfer_id=1) payload = {"from_organization_id": 1, "to_organization_id": 2} @@ -274,7 +278,8 @@ async def test_create_compliance_report_success( "create_compliance_report", organization_id=organization_id ) - payload = {"compliance_period": "2024", "organization_id": 1, "status": "status"} + payload = {"compliance_period": "2024", + "organization_id": 1, "status": "status"} mock_organization_validation.create_compliance_report.return_value = None mock_compliance_report_services.create_compliance_report.return_value = { @@ -346,7 +351,8 @@ async def test_get_all_org_reported_years_success( ): set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) - url = fastapi_app.url_path_for("get_all_org_reported_years", organization_id=1) + url = fastapi_app.url_path_for( + "get_all_org_reported_years", organization_id=1) mock_compliance_report_services.get_all_org_reported_years.return_value = [ {"compliance_period_id": 1, "description": "2024"} @@ -379,20 +385,23 @@ async def test_get_compliance_report_by_id_success( ) # Mock the compliance report service's method - mock_compliance_report_services.get_compliance_report_by_id.return_value = { - "compliance_report_id": 1, - "compliance_period_id": 1, - "compliance_period": {"compliance_period_id": 1, "description": "2024"}, - "organization_id": 1, - "organization": {"organization_id": 1, "name": "org1"}, - "current_status_id": 1, - "current_status": {"compliance_report_status_id": 1, "status": "status"}, - "summary": {"summary_id": 1, "is_locked": False}, - "compliance_report_group_uuid": "uuid", - "version": 0, - "supplemental_initiator": SupplementalInitiatorType.SUPPLIER_SUPPLEMENTAL, - "has_supplemental": False, - } + mock_compliance_report_services.get_compliance_report_by_id.return_value = ChainedComplianceReportSchema( + report={ + "compliance_report_id": 1, + "compliance_period_id": 1, + "compliance_period": {"compliance_period_id": 1, "description": "2024"}, + "organization_id": 1, + "organization": {"organization_id": 1, "name": "org1"}, + "current_status_id": 1, + "current_status": {"compliance_report_status_id": 1, "status": "status"}, + "summary": {"summary_id": 1, "is_locked": False}, + "compliance_report_group_uuid": "uuid", + "version": 0, + "supplemental_initiator": SupplementalInitiatorType.SUPPLIER_SUPPLEMENTAL, + "has_supplemental": False, + }, + chain=[] + ) # Create a mock for the validation service mock_compliance_report_validation = AsyncMock() @@ -412,7 +421,7 @@ async def test_get_compliance_report_by_id_success( # Assertions assert response.status_code == 200 mock_compliance_report_services.get_compliance_report_by_id.assert_awaited_once_with( - 1, apply_masking=True + 1, apply_masking=True, get_chain=True ) mock_compliance_report_validation.validate_organization_access.assert_awaited_once_with( 1 From 4391023d56df6f40bdda3e1123aa9e5d4ba7cd54 Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 3 Dec 2024 13:45:43 -0800 Subject: [PATCH 20/38] fix: missing redis instance fix --- backend/lcfs/services/tfrs/redis_balance.py | 36 ++++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/backend/lcfs/services/tfrs/redis_balance.py b/backend/lcfs/services/tfrs/redis_balance.py index 69dc96010..02d6f2a12 100644 --- a/backend/lcfs/services/tfrs/redis_balance.py +++ b/backend/lcfs/services/tfrs/redis_balance.py @@ -17,10 +17,19 @@ async def init_org_balance_cache(app: FastAPI): - redis = await app.state.redis_pool + """ + Initialize the organization balance cache and populate it with data. + + :param app: FastAPI application instance. + """ + # Get the Redis connection pool from app state + redis_pool: ConnectionPool = app.state.redis_pool + + # Create a Redis client using the connection pool + redis = Redis(connection_pool=redis_pool) + async with AsyncSession(async_engine) as session: async with session.begin(): - organization_repo = OrganizationsRepository(db=session) transaction_repo = TransactionRepository(db=session) @@ -29,23 +38,32 @@ async def init_org_balance_cache(app: FastAPI): # Get the current year current_year = datetime.now().year - logger.info(f"Starting balance cache population {current_year}") + logger.info(f"Starting balance cache population for {current_year}") + # Fetch all organizations all_orgs = await organization_repo.get_organizations() # Loop from the oldest year to the current year for year in range(int(oldest_year), current_year + 1): - # Call the function to process transactions for each year for org in all_orgs: + # Calculate the balance for each organization and year balance = ( await transaction_repo.calculate_available_balance_for_period( org.organization_id, year ) ) + # Set the balance in Redis await set_cache_value(org.organization_id, year, balance, redis) - logger.debug(f"Set balance for {org.name} for {year} to {balance}") + logger.debug( + f"Set balance for organization {org.name} " + f"for {year} to {balance}" + ) + logger.info(f"Cache populated with {len(all_orgs)} organizations") + # Close the Redis client + await redis.close() + class RedisBalanceService: def __init__( @@ -84,4 +102,12 @@ async def populate_organization_redis_balance( async def set_cache_value( organization_id: int, period: int, balance: int, redis: Redis ) -> None: + """ + Set a cache value in Redis for a specific organization and period. + + :param organization_id: ID of the organization. + :param period: The year or period for which the balance is being set. + :param balance: The balance value to set in the cache. + :param redis: Redis client instance. + """ await redis.set(name=f"balance_{organization_id}_{period}", value=balance) From 8da8ab728bbcd7a7f8792452f409978c9b893cc3 Mon Sep 17 00:00:00 2001 From: Kuan Fan Date: Tue, 3 Dec 2024 14:08:46 -0800 Subject: [PATCH 21/38] update prod ci --- .github/workflows/prod-ci.yaml | 31 +++++++++++++++++++++++++++---- .github/workflows/test-ci.yaml | 2 +- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prod-ci.yaml b/.github/workflows/prod-ci.yaml index f96807cc9..4bc17410f 100644 --- a/.github/workflows/prod-ci.yaml +++ b/.github/workflows/prod-ci.yaml @@ -42,16 +42,31 @@ jobs: echo "IMAGE_TAG retrieved from Test is $imagetag" echo "IMAGE_TAG=$imagetag" >> $GITHUB_OUTPUT + get-current-time: + name: Get Current Time + runs-on: ubuntu-latest + needs: get-image-tag + + outputs: + CURRENT_TIME: ${{ steps.get-current-time.outputs.CURRENT_TIME }} + + steps: + - id: get-current-time + run: | + TZ="America/Vancouver" + echo "CURRENT_TIME=$(date + '%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT + # Deplog the image which is running on test to prod deploy-on-prod: name: Deploy LCFS on Prod runs-on: ubuntu-latest - needs: get-image-tag + needs: [get-image-tag, get-current-time] timeout-minutes: 60 env: IMAGE_TAG: ${{ needs.get-image-tag.outputs.IMAGE_TAG }} + CURRENT_TIME: ${{ needs.get-current-time.outputs.CURRENT_TIME }} steps: @@ -66,9 +81,17 @@ jobs: uses: trstringer/manual-approval@v1.6.0 with: secret: ${{ github.TOKEN }} - approvers: AlexZorkin,kuanfandevops,hamed-valiollahi,airinggov,areyeslo,dhaselhan,Grulin,justin-lepitzki,kevin-hashimoto + approvers: AlexZorkin,kuanfandevops,hamed-valiollahi,airinggov,areyeslo,dhaselhan,Grulin minimum-approvals: 2 - issue-title: "LCFS ${{env.IMAGE_TAG }} Prod Deployment" + issue-title: "LCFS ${{env.IMAGE_TAG }} Prod Deployment. ${{ env.CURRENT_TIME }}" + + - name: Log in to Openshift + uses: redhat-actions/oc-login@v1.3 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} + insecure_skip_tls_verify: true + namespace: ${{ env.PROD_NAMESPACE }} - name: Tag LCFS images from Test to Prod run: | @@ -88,6 +111,6 @@ jobs: git config --global user.name "GitHub Actions" git add lcfs/charts/lcfs-frontend/values-prod.yaml git add lcfs/charts/lcfs-backend/values-prod.yaml - git commit -m "update the version with pre-release number for prod" + git commit -m "Update image tag ${{env.IMAGE_TAG }} for prod" git push \ No newline at end of file diff --git a/.github/workflows/test-ci.yaml b/.github/workflows/test-ci.yaml index e8ca4820d..1119b9432 100644 --- a/.github/workflows/test-ci.yaml +++ b/.github/workflows/test-ci.yaml @@ -225,7 +225,7 @@ jobs: uses: trstringer/manual-approval@v1.6.0 with: secret: ${{ github.TOKEN }} - approvers: AlexZorkin,kuanfandevops,hamed-valiollahi,airinggov,areyeslo,dhaselhan,Grulin,justin-lepitzki,kevin-hashimoto + approvers: AlexZorkin,kuanfandevops,hamed-valiollahi,airinggov,areyeslo,dhaselhan,Grulin,kevin-hashimoto minimum-approvals: 1 issue-title: "LCFS ${{ env.VERSION }}-${{ env.PRE_RELEASE }} Test Deployment" From 0a519328fe80b3262ef0b624e73f05a4ae1fab26 Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 3 Dec 2024 14:14:04 -0800 Subject: [PATCH 22/38] fix: await mock redis in test --- backend/lcfs/tests/services/tfrs/test_redis_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/lcfs/tests/services/tfrs/test_redis_balance.py b/backend/lcfs/tests/services/tfrs/test_redis_balance.py index 56ce31fb1..4d2f022b3 100644 --- a/backend/lcfs/tests/services/tfrs/test_redis_balance.py +++ b/backend/lcfs/tests/services/tfrs/test_redis_balance.py @@ -31,7 +31,7 @@ async def test_init_org_balance_cache(): async def mock_redis_pool(): return mock_redis - mock_app.state.redis_pool = mock_redis_pool() + mock_app.state.redis_pool = await mock_redis_pool() mock_app.state.settings = mock_settings current_year = datetime.now().year From dd98408163aa14ac3964b4c4358ceab7c10f5bc7 Mon Sep 17 00:00:00 2001 From: Kuan Fan Date: Tue, 3 Dec 2024 14:14:32 -0800 Subject: [PATCH 23/38] udpate get-current-time --- .github/workflows/prod-ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod-ci.yaml b/.github/workflows/prod-ci.yaml index 4bc17410f..3478be8ff 100644 --- a/.github/workflows/prod-ci.yaml +++ b/.github/workflows/prod-ci.yaml @@ -54,7 +54,7 @@ jobs: - id: get-current-time run: | TZ="America/Vancouver" - echo "CURRENT_TIME=$(date + '%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT + echo "CURRENT_TIME=$(date '+%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT # Deplog the image which is running on test to prod deploy-on-prod: @@ -83,7 +83,7 @@ jobs: secret: ${{ github.TOKEN }} approvers: AlexZorkin,kuanfandevops,hamed-valiollahi,airinggov,areyeslo,dhaselhan,Grulin minimum-approvals: 2 - issue-title: "LCFS ${{env.IMAGE_TAG }} Prod Deployment. ${{ env.CURRENT_TIME }}" + issue-title: "LCFS ${{env.IMAGE_TAG }} Prod Deployment at ${{ env.CURRENT_TIME }}." - name: Log in to Openshift uses: redhat-actions/oc-login@v1.3 From c7c3ce409adc0afe9f6f782ef73d50867e852584 Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 3 Dec 2024 14:44:35 -0800 Subject: [PATCH 24/38] fix: org balance test update --- .../tests/services/tfrs/test_redis_balance.py | 79 +++++++++---------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/backend/lcfs/tests/services/tfrs/test_redis_balance.py b/backend/lcfs/tests/services/tfrs/test_redis_balance.py index 4d2f022b3..9df7f20fc 100644 --- a/backend/lcfs/tests/services/tfrs/test_redis_balance.py +++ b/backend/lcfs/tests/services/tfrs/test_redis_balance.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import AsyncMock, patch, MagicMock +from unittest.mock import AsyncMock, patch, MagicMock, call from datetime import datetime from redis.asyncio import ConnectionPool, Redis @@ -13,55 +13,50 @@ @pytest.mark.anyio async def test_init_org_balance_cache(): - # Mock the session and repositories - mock_session = AsyncMock() - - # Mock the Redis client + # Mock the Redis connection pool + mock_redis_pool = AsyncMock() mock_redis = AsyncMock() - mock_redis.set = AsyncMock() # Ensure the `set` method is mocked - - # Mock the settings - mock_settings = MagicMock() - mock_settings.redis_url = "redis://localhost" - - # Create a mock app object - mock_app = MagicMock() - - # Simulate redis_pool as an awaitable returning mock_redis - async def mock_redis_pool(): - return mock_redis + mock_redis.set = AsyncMock() - mock_app.state.redis_pool = await mock_redis_pool() - mock_app.state.settings = mock_settings + # Ensure the `Redis` instance is created with the connection pool + with patch("lcfs.services.tfrs.redis_balance.Redis", return_value=mock_redis): + # Mock the app object + mock_app = MagicMock() + mock_app.state.redis_pool = mock_redis_pool - current_year = datetime.now().year - last_year = current_year - 1 + current_year = datetime.now().year + last_year = current_year - 1 - with patch( - "lcfs.web.api.organizations.services.OrganizationsRepository.get_organizations", - return_value=[ - MagicMock(organization_id=1, name="Org1"), - MagicMock(organization_id=2, name="Org2"), - ], - ): + # Mock repository methods with patch( + "lcfs.web.api.organizations.repo.OrganizationsRepository.get_organizations", + return_value=[ + MagicMock(organization_id=1, name="Org1"), + MagicMock(organization_id=2, name="Org2"), + ], + ), patch( "lcfs.web.api.transaction.repo.TransactionRepository.get_transaction_start_year", return_value=last_year, + ), patch( + "lcfs.web.api.transaction.repo.TransactionRepository.calculate_available_balance_for_period", + side_effect=[100, 200, 150, 250], ): - with patch( - "lcfs.web.api.transaction.repo.TransactionRepository.calculate_available_balance_for_period", - side_effect=[100, 200, 150, 250, 300, 350], - ): - # Pass the mock app to the function - await init_org_balance_cache(mock_app) - - # Assert that each cache set operation was called correctly - calls = mock_redis.set.mock_calls - assert len(calls) == 4 - mock_redis.set.assert_any_call(name=f"balance_1_{last_year}", value=100) - mock_redis.set.assert_any_call(name=f"balance_2_{last_year}", value=200) - mock_redis.set.assert_any_call(name=f"balance_1_{current_year}", value=150) - mock_redis.set.assert_any_call(name=f"balance_2_{current_year}", value=250) + # Execute the function with the mocked app + await init_org_balance_cache(mock_app) + + # Define expected calls to Redis `set` + expected_calls = [ + call(name=f"balance_1_{last_year}", value=100), + call(name=f"balance_2_{last_year}", value=200), + call(name=f"balance_1_{current_year}", value=150), + call(name=f"balance_2_{current_year}", value=250), + ] + + # Assert that Redis `set` method was called with the expected arguments + mock_redis.set.assert_has_calls(expected_calls, any_order=True) + + # Ensure the number of calls matches the expected count + assert mock_redis.set.call_count == len(expected_calls) @pytest.mark.anyio From db57b3da3bed3dd28f15a6fbfb0631c84377b2f5 Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 3 Dec 2024 16:01:57 -0800 Subject: [PATCH 25/38] fix: lifetime cache fix, jwk refresh cache updated for redis_pool --- .../lcfs/services/keycloak/authentication.py | 71 +++++++++++-------- backend/lcfs/web/lifetime.py | 9 ++- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/backend/lcfs/services/keycloak/authentication.py b/backend/lcfs/services/keycloak/authentication.py index 86026ba6f..f3fc1f776 100644 --- a/backend/lcfs/services/keycloak/authentication.py +++ b/backend/lcfs/services/keycloak/authentication.py @@ -2,9 +2,8 @@ import httpx import jwt -from fastapi import HTTPException, Depends -from redis import ConnectionPool -from redis.asyncio import Redis +from fastapi import HTTPException +from redis.asyncio import Redis, ConnectionPool from sqlalchemy import func from sqlalchemy.exc import NoResultFound from sqlalchemy.ext.asyncio import async_sessionmaker @@ -27,7 +26,7 @@ class UserAuthentication(AuthenticationBackend): def __init__( self, - redis_pool: Redis, + redis_pool: ConnectionPool, session_factory: async_sessionmaker, settings: Settings, ): @@ -39,30 +38,46 @@ def __init__( self.test_keycloak_user = None async def refresh_jwk(self): - # Try to get the JWKS data from Redis cache - jwks_data = await self.redis_pool.get("jwks_data") - - if jwks_data: - jwks_data = json.loads(jwks_data) - self.jwks = jwks_data.get("jwks") - self.jwks_uri = jwks_data.get("jwks_uri") - return - - # If not in cache, retrieve from the well-known endpoint - async with httpx.AsyncClient() as client: - oidc_response = await client.get(self.settings.well_known_endpoint) - jwks_uri = oidc_response.json().get("jwks_uri") - certs_response = await client.get(jwks_uri) - jwks = certs_response.json() - - # Composite object containing both JWKS and JWKS URI - jwks_data = {"jwks": jwks, "jwks_uri": jwks_uri} - - # Cache the composite JWKS data with a TTL of 1 day (86400 seconds) - await self.redis_pool.set("jwks_data", json.dumps(jwks_data), ex=86400) - - self.jwks = jwks - self.jwks_uri = jwks_uri + """ + Refreshes the JSON Web Key (JWK) used for token verification. + This method attempts to retrieve the JWK from Redis cache. + If not found, it fetches it from the well-known endpoint + and stores it in Redis for future use. + """ + # Create a Redis client from the connection pool + async with Redis(connection_pool=self.redis_pool) as redis: + # Try to get the JWKS data from Redis cache + jwks_data = await redis.get("jwks_data") + + if jwks_data: + jwks_data = json.loads(jwks_data) + self.jwks = jwks_data.get("jwks") + self.jwks_uri = jwks_data.get("jwks_uri") + return + + # If not in cache, retrieve from the well-known endpoint + async with httpx.AsyncClient() as client: + oidc_response = await client.get(self.settings.well_known_endpoint) + oidc_response.raise_for_status() + jwks_uri = oidc_response.json().get("jwks_uri") + + if not jwks_uri: + raise ValueError( + "JWKS URI not found in the well-known endpoint response." + ) + + certs_response = await client.get(jwks_uri) + certs_response.raise_for_status() + jwks = certs_response.json() + + # Composite object containing both JWKS and JWKS URI + jwks_data = {"jwks": jwks, "jwks_uri": jwks_uri} + + # Cache the composite JWKS data with a TTL of 1 day (86400 seconds) + await redis.set("jwks_data", json.dumps(jwks_data), ex=86400) + + self.jwks = jwks + self.jwks_uri = jwks_uri async def authenticate(self, request): # Extract the authorization header from the request diff --git a/backend/lcfs/web/lifetime.py b/backend/lcfs/web/lifetime.py index 0215689e7..cbeb556b2 100644 --- a/backend/lcfs/web/lifetime.py +++ b/backend/lcfs/web/lifetime.py @@ -4,7 +4,7 @@ import boto3 from fastapi_cache import FastAPICache from fastapi_cache.backends.redis import RedisBackend -from redis import asyncio as aioredis +from redis.asyncio import Redis from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from lcfs.services.rabbitmq.consumers import start_consumers, stop_consumers @@ -57,8 +57,11 @@ async def _startup() -> None: # noqa: WPS430 # Assign settings to app state for global access app.state.settings = settings - # Initialize the cache with Redis backend using app.state.redis_pool - FastAPICache.init(RedisBackend(app.state.redis_pool), prefix="lcfs") + # Create a Redis client from the connection pool + redis_client = Redis(connection_pool=app.state.redis_pool) + + # Initialize FastAPI cache with the Redis client + FastAPICache.init(RedisBackend(redis_client), prefix="lcfs") await init_org_balance_cache(app) From a2fe547d34478188eacebe2687f0bd4bcbc3e16f Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 3 Dec 2024 16:31:36 -0800 Subject: [PATCH 26/38] chore: auth middleware test updates --- backend/lcfs/tests/test_auth_middleware.py | 129 ++++++++++++++++----- 1 file changed, 100 insertions(+), 29 deletions(-) diff --git a/backend/lcfs/tests/test_auth_middleware.py b/backend/lcfs/tests/test_auth_middleware.py index d59076107..083c188a5 100644 --- a/backend/lcfs/tests/test_auth_middleware.py +++ b/backend/lcfs/tests/test_auth_middleware.py @@ -1,9 +1,11 @@ from unittest.mock import AsyncMock, patch, MagicMock, Mock import pytest -import asyncio +import json +import redis from starlette.exceptions import HTTPException from starlette.requests import Request +from redis.asyncio import Redis, ConnectionPool from lcfs.db.models import UserProfile from lcfs.services.keycloak.authentication import UserAuthentication @@ -35,43 +37,112 @@ def auth_backend(redis_pool, session_generator, settings): @pytest.mark.anyio -async def test_load_jwk_from_redis(auth_backend): - # Mock auth_backend.redis_pool.get to return a JSON string directly - with patch.object(auth_backend.redis_pool, "get", new_callable=AsyncMock) as mock_redis_get: - mock_redis_get.return_value = '{"jwks": "jwks", "jwks_uri": "jwks_uri"}' +async def test_load_jwk_from_redis(): + # Initialize mock Redis client + mock_redis = AsyncMock(spec=Redis) + mock_redis.get = AsyncMock( + return_value='{"jwks": "jwks_data", "jwks_uri": "jwks_uri_data"}' + ) + # Mock the async context manager (__aenter__ and __aexit__) + mock_redis.__aenter__.return_value = mock_redis + mock_redis.__aexit__.return_value = AsyncMock() + + # Initialize mock ConnectionPool + mock_redis_pool = AsyncMock(spec=ConnectionPool) + + # Patch the Redis class in the UserAuthentication module to return mock_redis + with patch("lcfs.services.keycloak.authentication.Redis", return_value=mock_redis): + # Initialize UserAuthentication with the mocked ConnectionPool + auth_backend = UserAuthentication( + redis_pool=mock_redis_pool, + session_factory=AsyncMock(), + settings=MagicMock( + well_known_endpoint="https://example.com/.well-known/openid-configuration" + ), + ) + + # Call refresh_jwk await auth_backend.refresh_jwk() - assert auth_backend.jwks == "jwks" - assert auth_backend.jwks_uri == "jwks_uri" + # Assertions to verify JWKS data was loaded correctly + assert auth_backend.jwks == "jwks_data" + assert auth_backend.jwks_uri == "jwks_uri_data" + + # Verify that Redis `get` was called with the correct key + mock_redis.get.assert_awaited_once_with("jwks_data") @pytest.mark.anyio @patch("httpx.AsyncClient.get") -async def test_refresh_jwk_sets_new_keys_in_redis(mock_get, auth_backend): - # Create a mock response object - mock_response = MagicMock() - - # Set up the json method to return a dictionary with a .get method - mock_json = MagicMock() - mock_json.get.return_value = "{}" - - # Assign the mock_json to the json method of the response - mock_response.json.return_value = mock_json - - mock_response_2 = MagicMock() - mock_response_2.json.return_value = "{}" - - mock_get.side_effect = [ - mock_response, - mock_response_2, - ] - - with patch.object(auth_backend.redis_pool, "get", new_callable=AsyncMock) as mock_redis_get: - mock_redis_get.return_value = None - +async def test_refresh_jwk_sets_new_keys_in_redis(mock_httpx_get): + # Mock responses for the well-known endpoint and JWKS URI + mock_oidc_response = MagicMock() + mock_oidc_response.json.return_value = {"jwks_uri": "https://example.com/jwks"} + mock_oidc_response.raise_for_status = MagicMock() + + mock_certs_response = MagicMock() + mock_certs_response.json.return_value = { + "keys": [{"kty": "RSA", "kid": "key2", "use": "sig", "n": "def", "e": "AQAB"}] + } + mock_certs_response.raise_for_status = MagicMock() + + # Configure the mock to return the above responses in order + mock_httpx_get.side_effect = [mock_oidc_response, mock_certs_response] + + # Initialize mock Redis client + mock_redis = AsyncMock(spec=Redis) + mock_redis.get = AsyncMock(return_value=None) # JWKS data not in cache + mock_redis.set = AsyncMock() + + # Mock the async context manager (__aenter__ and __aexit__) + mock_redis.__aenter__.return_value = mock_redis + mock_redis.__aexit__.return_value = AsyncMock() + + # Initialize mock ConnectionPool + mock_redis_pool = AsyncMock(spec=ConnectionPool) + + # Patch the Redis class in the UserAuthentication module to return mock_redis + with patch("lcfs.services.keycloak.authentication.Redis", return_value=mock_redis): + # Initialize UserAuthentication with the mocked ConnectionPool + auth_backend = UserAuthentication( + redis_pool=mock_redis_pool, + session_factory=AsyncMock(), + settings=MagicMock( + well_known_endpoint="https://example.com/.well-known/openid-configuration" + ), + ) + + # Call refresh_jwk await auth_backend.refresh_jwk() + # Assertions to verify JWKS data was fetched and set correctly + expected_jwks = { + "keys": [ + {"kty": "RSA", "kid": "key2", "use": "sig", "n": "def", "e": "AQAB"} + ] + } + assert auth_backend.jwks == expected_jwks + assert auth_backend.jwks_uri == "https://example.com/jwks" + + # Verify that Redis `get` was called with "jwks_data" + mock_redis.get.assert_awaited_once_with("jwks_data") + + # Verify that the well-known endpoint was called twice + assert mock_httpx_get.call_count == 2 + mock_httpx_get.assert_any_call( + "https://example.com/.well-known/openid-configuration" + ) + mock_httpx_get.assert_any_call("https://example.com/jwks") + + # Verify that Redis `set` was called with the correct parameters + expected_jwks_data = { + "jwks": expected_jwks, + "jwks_uri": "https://example.com/jwks", + } + mock_redis.set.assert_awaited_once_with( + "jwks_data", json.dumps(expected_jwks_data), ex=86400 + ) @pytest.mark.anyio From fd9d0bb3a74976c92710b50870f17a1aee0cd716 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Wed, 4 Dec 2024 06:35:55 -0700 Subject: [PATCH 27/38] Error validation message labels in AG Grid --- backend/lcfs/web/api/allocation_agreement/schema.py | 8 +++++--- backend/lcfs/web/api/fuel_export/schema.py | 13 +++++++------ backend/lcfs/web/api/fuel_supply/schema.py | 12 ++++++------ backend/lcfs/web/api/other_uses/schema.py | 6 +++--- frontend/src/assets/locales/en/fuelExport.json | 6 +++--- frontend/src/assets/locales/en/fuelSupply.json | 2 +- .../AddAllocationAgreements.jsx | 8 -------- frontend/src/views/FuelExports/_schema.jsx | 6 +++--- frontend/src/views/FuelSupplies/_schema.jsx | 2 +- 9 files changed, 29 insertions(+), 34 deletions(-) diff --git a/backend/lcfs/web/api/allocation_agreement/schema.py b/backend/lcfs/web/api/allocation_agreement/schema.py index f5ffe9179..c2079fe2a 100644 --- a/backend/lcfs/web/api/allocation_agreement/schema.py +++ b/backend/lcfs/web/api/allocation_agreement/schema.py @@ -58,6 +58,7 @@ class AllocationAgreementTableOptionsSchema(BaseSchema): class AllocationAgreementCreateSchema(BaseSchema): compliance_report_id: int allocation_agreement_id: Optional[int] = None + allocation_transaction_type: str transaction_partner: str postal_address: str transaction_partner_email: str @@ -65,11 +66,12 @@ class AllocationAgreementCreateSchema(BaseSchema): fuel_type: str fuel_type_other: Optional[str] = None ci_of_fuel: float - quantity: int + provision_of_the_act: str + quantity: int = Field( + ..., gt=0, description="Quantity must be greater than 0" + ) units: str - allocation_transaction_type: str fuel_category: str - provision_of_the_act: Optional[str] = None fuel_code: Optional[str] = None deleted: Optional[bool] = None diff --git a/backend/lcfs/web/api/fuel_export/schema.py b/backend/lcfs/web/api/fuel_export/schema.py index babf09f6c..654975327 100644 --- a/backend/lcfs/web/api/fuel_export/schema.py +++ b/backend/lcfs/web/api/fuel_export/schema.py @@ -136,6 +136,12 @@ class FuelExportSchema(BaseSchema): compliance_period: Optional[str] = None fuel_type_id: int fuel_type: FuelTypeSchema + fuel_category_id: int + fuel_category: FuelCategoryResponseSchema + end_use_id: Optional[int] = None + end_use_type: Optional[EndUseTypeSchema] = None + provision_of_the_act_id: Optional[int] = None + provision_of_the_act: Optional[ProvisionOfTheActSchema] = None fuel_type_other: Optional[str] = None quantity: int = Field(..., gt=0) units: str @@ -146,14 +152,9 @@ class FuelExportSchema(BaseSchema): energy_density: Optional[float] = None eer: Optional[float] = None energy: Optional[float] = None - fuel_category_id: int - fuel_category: FuelCategoryResponseSchema fuel_code_id: Optional[int] = None fuel_code: Optional[FuelCodeResponseSchema] = None - provision_of_the_act_id: Optional[int] = None - provision_of_the_act: Optional[ProvisionOfTheActSchema] = None - end_use_id: Optional[int] = None - end_use_type: Optional[EndUseTypeSchema] = None + @validator("quantity") def quantity_must_be_positive(cls, v): diff --git a/backend/lcfs/web/api/fuel_supply/schema.py b/backend/lcfs/web/api/fuel_supply/schema.py index 155b7998f..c5e5a2186 100644 --- a/backend/lcfs/web/api/fuel_supply/schema.py +++ b/backend/lcfs/web/api/fuel_supply/schema.py @@ -161,6 +161,12 @@ class FuelSupplyResponseSchema(BaseSchema): action_type: str fuel_type_id: int fuel_type: FuelTypeSchema + fuel_category_id: Optional[int] = None + fuel_category: FuelCategoryResponseSchema + end_use_id: Optional[int] = None + end_use_type: Optional[EndUseTypeSchema] = None + provision_of_the_act_id: Optional[int] = None + provision_of_the_act: Optional[ProvisionOfTheActSchema] = None compliance_period: Optional[str] = None quantity: int units: str @@ -170,14 +176,8 @@ class FuelSupplyResponseSchema(BaseSchema): energy_density: Optional[float] = None eer: Optional[float] = None energy: Optional[float] = None - fuel_category_id: Optional[int] = None - fuel_category: FuelCategoryResponseSchema fuel_code_id: Optional[int] = None fuel_code: Optional[FuelCodeResponseSchema] = None - provision_of_the_act_id: Optional[int] = None - provision_of_the_act: Optional[ProvisionOfTheActSchema] = None - end_use_id: Optional[int] = None - end_use_type: Optional[EndUseTypeSchema] = None fuel_type_other: Optional[str] = None diff --git a/backend/lcfs/web/api/other_uses/schema.py b/backend/lcfs/web/api/other_uses/schema.py index b13f359e8..7b21dfa1b 100644 --- a/backend/lcfs/web/api/other_uses/schema.py +++ b/backend/lcfs/web/api/other_uses/schema.py @@ -85,13 +85,13 @@ class OtherUsesTableOptionsSchema(BaseSchema): class OtherUsesCreateSchema(BaseSchema): other_uses_id: Optional[int] = None compliance_report_id: int - quantity_supplied: int fuel_type: str fuel_category: str - expected_use: str provision_of_the_act: str - fuel_code: Optional[str] = None + quantity_supplied: int units: str + expected_use: str + fuel_code: Optional[str] = None ci_of_fuel: Optional[float] = None expected_use: str other_uses_id: Optional[int] = None diff --git a/frontend/src/assets/locales/en/fuelExport.json b/frontend/src/assets/locales/en/fuelExport.json index 2f5929e32..041a8deba 100644 --- a/frontend/src/assets/locales/en/fuelExport.json +++ b/frontend/src/assets/locales/en/fuelExport.json @@ -12,12 +12,12 @@ "fuelExportColLabels": { "complianceReportId": "Compliance Report ID", "fuelExportId": "Fuel export ID", - "fuelType": "Fuel type", + "fuelTypeId": "Fuel type", "exportDate": "Export date", "fuelTypeOther": "Fuel type other", - "fuelCategory": "Fuel catgory", + "fuelCategoryId": "Fuel catgory", "endUse": "End use", - "provisionOfTheAct": "Determining carbon intensity", + "provisionOfTheActId": "Determining carbon intensity", "fuelCode": "Fuel code", "quantity": "Quantity supplied", "units": "Units", diff --git a/frontend/src/assets/locales/en/fuelSupply.json b/frontend/src/assets/locales/en/fuelSupply.json index 9a7510d6e..cfc69302c 100644 --- a/frontend/src/assets/locales/en/fuelSupply.json +++ b/frontend/src/assets/locales/en/fuelSupply.json @@ -17,7 +17,7 @@ "fuelTypeOther": "Fuel type other", "fuelCategory": "Fuel category", "endUse": "End use", - "provisionOfTheAct": "Determining carbon intensity", + "provisionOfTheActId": "Determining carbon intensity", "fuelCode": "Fuel code", "quantity": "Quantity supplied", "units": "Units", diff --git a/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx b/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx index 2a9d258bf..dd37955ad 100644 --- a/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx +++ b/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx @@ -134,14 +134,6 @@ export const AddEditAllocationAgreements = () => { async (params) => { if (params.oldValue === params.newValue) return - if (!params.data.provisionOfTheAct) { - alertRef.current?.triggerAlert({ - message: 'Determining Carbon Intensity field is required.', - severity: 'error' - }) - return - } - params.node.updateData({ ...params.node.data, validationStatus: 'pending' diff --git a/frontend/src/views/FuelExports/_schema.jsx b/frontend/src/views/FuelExports/_schema.jsx index ead5d7394..2d61fcb71 100644 --- a/frontend/src/views/FuelExports/_schema.jsx +++ b/frontend/src/views/FuelExports/_schema.jsx @@ -108,7 +108,7 @@ export const fuelExportColDefs = (optionsData, errors) => [ { field: 'fuelType', headerComponent: RequiredHeader, - headerName: i18n.t('fuelExport:fuelExportColLabels.fuelType'), + headerName: i18n.t('fuelExport:fuelExportColLabels.fuelTypeId'), cellEditor: AutocompleteCellEditor, cellRenderer: (params) => params.value || @@ -177,7 +177,7 @@ export const fuelExportColDefs = (optionsData, errors) => [ { field: 'fuelCategory', headerComponent: RequiredHeader, - headerName: i18n.t('fuelExport:fuelExportColLabels.fuelCategory'), + headerName: i18n.t('fuelExport:fuelExportColLabels.fuelCategoryId'), cellEditor: AutocompleteCellEditor, cellRenderer: (params) => params.value || @@ -284,7 +284,7 @@ export const fuelExportColDefs = (optionsData, errors) => [ { field: 'provisionOfTheAct', headerComponent: RequiredHeader, - headerName: i18n.t('fuelExport:fuelExportColLabels.provisionOfTheAct'), + headerName: i18n.t('fuelExport:fuelExportColLabels.provisionOfTheActId'), cellEditor: 'agSelectCellEditor', cellRenderer: (params) => params.value || diff --git a/frontend/src/views/FuelSupplies/_schema.jsx b/frontend/src/views/FuelSupplies/_schema.jsx index cae05b9f5..33054f558 100644 --- a/frontend/src/views/FuelSupplies/_schema.jsx +++ b/frontend/src/views/FuelSupplies/_schema.jsx @@ -256,7 +256,7 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ { field: 'provisionOfTheAct', headerComponent: RequiredHeader, - headerName: i18n.t('fuelSupply:fuelSupplyColLabels.provisionOfTheAct'), + headerName: i18n.t('fuelSupply:fuelSupplyColLabels.provisionOfTheActId'), cellEditor: 'agSelectCellEditor', cellRenderer: (params) => params.value || From de2f20e24614231e8ab0aa318401b88ef8bdf925 Mon Sep 17 00:00:00 2001 From: prv-proton Date: Wed, 4 Dec 2024 10:01:14 -0800 Subject: [PATCH 28/38] . --- frontend/src/utils/grid/cellRenderers.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index dc2a5e0e7..7563007f4 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -549,11 +549,7 @@ export const RoleRenderer = (props) => { .filter((role) => role !== roles.government && role !== roles.supplier) useEffect(() => { - setIsGovernmentRole( - Array.isArray(value) - ? value.includes(roles.government) - : value.includes(roles.government) - ) + setIsGovernmentRole(value.includes(roles.government)) }, [value]) return ( From c96f616301a8b277e8e341e7a86811c2df8f9ab4 Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Wed, 4 Dec 2024 10:52:49 -0800 Subject: [PATCH 29/38] fix: hide draft status history --- .../views/ComplianceReports/components/AssessmentCard.jsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx index d5f2a055b..91658562d 100644 --- a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx +++ b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx @@ -76,12 +76,8 @@ const HistoryCard = ({ report }) => { } return item }) - .filter( - (item) => - item.status.status !== COMPLIANCE_REPORT_STATUSES.DRAFT || - report.hasSupplemental - ) - }, [isGovernmentUser, report.hasSupplemental, report.history]) + .filter((item) => item.status.status !== COMPLIANCE_REPORT_STATUSES.DRAFT) + }, [isGovernmentUser, report.history]) return ( Date: Wed, 4 Dec 2024 12:53:38 -0800 Subject: [PATCH 30/38] chore: refactor redis app.state to use Redis instance instead of ConnectionPool --- backend/lcfs/__main__.py | 1 + backend/lcfs/conftest.py | 27 +++-- backend/lcfs/db/dependencies.py | 13 +-- .../common/allocation_agreement_seeder.py | 1 - .../lcfs/services/keycloak/authentication.py | 16 +-- .../services/rabbitmq/transaction_consumer.py | 6 +- backend/lcfs/services/redis/dependency.py | 22 ++-- backend/lcfs/services/redis/lifetime.py | 35 +++--- backend/lcfs/services/tfrs/redis_balance.py | 23 ++-- backend/lcfs/settings.py | 8 +- backend/lcfs/tests/conftest.py | 13 +++ .../lcfs/tests/services/redis/test_redis.py | 101 +++++++++--------- .../tests/services/tfrs/test_redis_balance.py | 23 ++-- backend/lcfs/tests/test_auth_middleware.py | 17 +-- backend/lcfs/web/api/redis/__init__.py | 5 - backend/lcfs/web/api/redis/schema.py | 10 -- backend/lcfs/web/api/redis/views.py | 44 -------- backend/lcfs/web/api/router.py | 2 - backend/lcfs/web/application.py | 6 +- backend/lcfs/web/lifetime.py | 5 +- 20 files changed, 156 insertions(+), 222 deletions(-) delete mode 100644 backend/lcfs/web/api/redis/__init__.py delete mode 100644 backend/lcfs/web/api/redis/schema.py delete mode 100644 backend/lcfs/web/api/redis/views.py diff --git a/backend/lcfs/__main__.py b/backend/lcfs/__main__.py index 397e0a313..146f7cff8 100644 --- a/backend/lcfs/__main__.py +++ b/backend/lcfs/__main__.py @@ -14,6 +14,7 @@ def main() -> None: reload=settings.reload, log_level=settings.log_level.value.lower(), factory=True, + timeout_keep_alive=settings.timeout_keep_alive, ) except Exception as e: print(e) diff --git a/backend/lcfs/conftest.py b/backend/lcfs/conftest.py index 69c244a6e..cea7bae8a 100644 --- a/backend/lcfs/conftest.py +++ b/backend/lcfs/conftest.py @@ -37,7 +37,7 @@ from lcfs.db.models.user.UserRole import UserRole from lcfs.db.seeders.seed_database import seed_database from lcfs.db.utils import create_test_database, drop_test_database -from lcfs.services.redis.dependency import get_redis_pool +from lcfs.services.redis.dependency import get_redis_client from lcfs.settings import settings from lcfs.web.application import get_app @@ -118,19 +118,20 @@ async def dbsession( @pytest.fixture -async def fake_redis_pool() -> AsyncGenerator[ConnectionPool, None]: +async def fake_redis_client() -> AsyncGenerator[aioredis.FakeRedis, None]: """ - Get instance of a fake redis. + Get instance of a fake Redis client. - :yield: FakeRedis instance. + :yield: FakeRedis client instance. """ server = FakeServer() server.connected = True - pool = ConnectionPool(connection_class=FakeConnection, server=server) + redis_client = aioredis.FakeRedis(server=server, decode_responses=True) - yield pool - - await pool.disconnect() + try: + yield redis_client + finally: + await redis_client.close() @pytest.fixture @@ -153,26 +154,24 @@ async def dbsession_factory( @pytest.fixture def fastapi_app( dbsession: AsyncSession, - fake_redis_pool: ConnectionPool, + fake_redis_client: aioredis.FakeRedis, set_mock_user, # Fixture for setting up mock authentication user_roles: List[RoleEnum] = [RoleEnum.ADMINISTRATOR], # Default role ) -> FastAPI: # Create the FastAPI application instance application = get_app() application.dependency_overrides[get_async_db_session] = lambda: dbsession - application.dependency_overrides[get_redis_pool] = lambda: fake_redis_pool + application.dependency_overrides[get_redis_client] = lambda: fake_redis_client # Set up application state for testing - application.state.redis_pool = fake_redis_pool - # application.state.db_session_factory = test_session_factory + application.state.redis_client = fake_redis_client application.state.settings = settings # Set up mock authentication backend with the specified roles set_mock_user(application, user_roles) # Initialize the cache with fake Redis backend - fake_redis = aioredis.FakeRedis(connection_pool=fake_redis_pool) - FastAPICache.init(RedisBackend(fake_redis), prefix="lcfs") + FastAPICache.init(RedisBackend(fake_redis_client), prefix="lcfs") return application diff --git a/backend/lcfs/db/dependencies.py b/backend/lcfs/db/dependencies.py index dfcd2f393..b78c885a2 100644 --- a/backend/lcfs/db/dependencies.py +++ b/backend/lcfs/db/dependencies.py @@ -17,6 +17,7 @@ async_engine = create_async_engine(db_url, future=True) logging.getLogger("sqlalchemy.engine").setLevel(logging.WARN) + async def set_user_context(session: AsyncSession, username: str): """ Set user_id context for the session to be used in auditing. @@ -49,15 +50,3 @@ async def get_async_db_session(request: Request) -> AsyncGenerator[AsyncSession, raise e finally: await session.close() # Always close the session to free up the connection - - -def create_redis(): - return aioredis.ConnectionPool( - host=settings.redis_host, - port=settings.redis_port, - db=settings.redis_db, - decode_responses=True, - ) - - -pool = create_redis() diff --git a/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py b/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py index 8275d1b1a..e78fc2bbc 100644 --- a/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py +++ b/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py @@ -43,7 +43,6 @@ async def seed_allocation_transaction_types(session): transaction_type = AllocationTransactionType(**type_data) session.add(transaction_type) - logger.info("Successfully seeded allocation transaction types.") except Exception as e: context = { "function": "seed_allocation_transaction_types", diff --git a/backend/lcfs/services/keycloak/authentication.py b/backend/lcfs/services/keycloak/authentication.py index f3fc1f776..dd6d51f29 100644 --- a/backend/lcfs/services/keycloak/authentication.py +++ b/backend/lcfs/services/keycloak/authentication.py @@ -26,13 +26,13 @@ class UserAuthentication(AuthenticationBackend): def __init__( self, - redis_pool: ConnectionPool, + redis_client: Redis, session_factory: async_sessionmaker, settings: Settings, ): self.session_factory = session_factory self.settings = settings - self.redis_pool = redis_pool + self.redis_client = redis_client self.jwks = None self.jwks_uri = None self.test_keycloak_user = None @@ -44,10 +44,9 @@ async def refresh_jwk(self): If not found, it fetches it from the well-known endpoint and stores it in Redis for future use. """ - # Create a Redis client from the connection pool - async with Redis(connection_pool=self.redis_pool) as redis: + try: # Try to get the JWKS data from Redis cache - jwks_data = await redis.get("jwks_data") + jwks_data = await self.redis_client.get("jwks_data") if jwks_data: jwks_data = json.loads(jwks_data) @@ -74,11 +73,16 @@ async def refresh_jwk(self): jwks_data = {"jwks": jwks, "jwks_uri": jwks_uri} # Cache the composite JWKS data with a TTL of 1 day (86400 seconds) - await redis.set("jwks_data", json.dumps(jwks_data), ex=86400) + await self.redis_client.set("jwks_data", json.dumps(jwks_data), ex=86400) self.jwks = jwks self.jwks_uri = jwks_uri + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error refreshing JWK: {str(e)}" + ) + async def authenticate(self, request): # Extract the authorization header from the request auth = request.headers.get("Authorization") diff --git a/backend/lcfs/services/rabbitmq/transaction_consumer.py b/backend/lcfs/services/rabbitmq/transaction_consumer.py index 09626551a..381142d6c 100644 --- a/backend/lcfs/services/rabbitmq/transaction_consumer.py +++ b/backend/lcfs/services/rabbitmq/transaction_consumer.py @@ -4,7 +4,7 @@ from redis.asyncio import Redis from sqlalchemy.ext.asyncio import AsyncSession -from lcfs.services.redis.dependency import get_redis_pool +from lcfs.services.redis.dependency import get_redis_client from fastapi import Request from lcfs.db.dependencies import async_engine @@ -50,14 +50,14 @@ async def process_message(self, body: bytes, request: Request): compliance_units = message_content.get("compliance_units_amount") org_id = message_content.get("organization_id") - redis_pool = await get_redis_pool(request) + redis_client = await get_redis_client(request) async with AsyncSession(async_engine) as session: async with session.begin(): repo = OrganizationsRepository(db=session) transaction_repo = TransactionRepository(db=session) redis_balance_service = RedisBalanceService( - transaction_repo=transaction_repo, redis_pool=redis_pool + transaction_repo=transaction_repo, redis_client=redis_client ) org_service = OrganizationsService( repo=repo, diff --git a/backend/lcfs/services/redis/dependency.py b/backend/lcfs/services/redis/dependency.py index 14a19190c..01cc689eb 100644 --- a/backend/lcfs/services/redis/dependency.py +++ b/backend/lcfs/services/redis/dependency.py @@ -1,23 +1,19 @@ -from redis.asyncio import ConnectionPool +from redis.asyncio import Redis from starlette.requests import Request -# Redis Pool Dependency -async def get_redis_pool( +# Redis Client Dependency +async def get_redis_client( request: Request, -) -> ConnectionPool: +) -> Redis: """ - Returns the Redis connection pool. + Returns the Redis client. Usage: - >>> from redis.asyncio import ConnectionPool, Redis - >>> - >>> async def handler(redis_pool: ConnectionPool = Depends(get_redis_pool)): - >>> redis = Redis(connection_pool=redis_pool) - >>> await redis.get('key') - >>> await redis.close() + >>> async def handler(redis_client: Redis = Depends(get_redis_client)): + >>> value = await redis_client.get('key') :param request: Current request object. - :returns: Redis connection pool. + :returns: Redis client. """ - return request.app.state.redis_pool + return request.app.state.redis_client diff --git a/backend/lcfs/services/redis/lifetime.py b/backend/lcfs/services/redis/lifetime.py index 2e007adbd..6d4a5f2eb 100644 --- a/backend/lcfs/services/redis/lifetime.py +++ b/backend/lcfs/services/redis/lifetime.py @@ -1,6 +1,6 @@ import logging from fastapi import FastAPI -from redis.asyncio import ConnectionPool, Redis +from redis.asyncio import Redis from redis.exceptions import RedisError from lcfs.settings import settings @@ -10,22 +10,25 @@ async def init_redis(app: FastAPI) -> None: """ - Creates connection pool for redis. + Initializes the Redis client and tests the connection. - :param app: current fastapi application. + :param app: current FastAPI application. """ try: - app.state.redis_pool = ConnectionPool.from_url( - str(settings.redis_url), - encoding="utf8", + # Initialize Redis client directly + app.state.redis_client = Redis( + host=settings.redis_host, + port=settings.redis_port, + password=settings.redis_pass, + db=settings.redis_base or 0, decode_responses=True, - max_connections=200, + socket_timeout=5, # Timeout for socket read/write (seconds) + socket_connect_timeout=5, # Timeout for connection establishment (seconds) ) + # Test the connection - redis = Redis(connection_pool=app.state.redis_pool) - await redis.ping() - await redis.close() - logger.info("Redis pool initialized successfully.") + await app.state.redis_client.ping() + logger.info("Redis client initialized and connection successful.") except RedisError as e: logger.error(f"Redis error during initialization: {e}") raise @@ -34,16 +37,16 @@ async def init_redis(app: FastAPI) -> None: raise -async def shutdown_redis(app: FastAPI) -> None: # pragma: no cover +async def shutdown_redis(app: FastAPI) -> None: """ - Closes redis connection pool. + Closes the Redis client during application shutdown. :param app: current FastAPI app. """ try: - if hasattr(app.state, "redis_pool"): - await app.state.redis_pool.disconnect(inuse_connections=True) - logger.info("Redis pool closed successfully.") + if hasattr(app.state, "redis_client") and app.state.redis_client: + await app.state.redis_client.close() + logger.info("Redis client closed successfully.") except RedisError as e: logger.error(f"Redis error during shutdown: {e}") except Exception as e: diff --git a/backend/lcfs/services/tfrs/redis_balance.py b/backend/lcfs/services/tfrs/redis_balance.py index 02d6f2a12..2628a831a 100644 --- a/backend/lcfs/services/tfrs/redis_balance.py +++ b/backend/lcfs/services/tfrs/redis_balance.py @@ -1,12 +1,12 @@ import logging from datetime import datetime -from fastapi import FastAPI, Depends, Request -from redis.asyncio import Redis, ConnectionPool +from fastapi import FastAPI, Depends +from redis.asyncio import Redis from sqlalchemy.ext.asyncio import AsyncSession from lcfs.db.dependencies import async_engine -from lcfs.services.redis.dependency import get_redis_pool +from lcfs.services.redis.dependency import get_redis_client from lcfs.settings import settings from lcfs.web.api.organizations.repo import OrganizationsRepository from lcfs.web.api.transaction.repo import TransactionRepository @@ -22,11 +22,8 @@ async def init_org_balance_cache(app: FastAPI): :param app: FastAPI application instance. """ - # Get the Redis connection pool from app state - redis_pool: ConnectionPool = app.state.redis_pool - - # Create a Redis client using the connection pool - redis = Redis(connection_pool=redis_pool) + # Get the Redis client from app state + redis: Redis = app.state.redis_client async with AsyncSession(async_engine) as session: async with session.begin(): @@ -61,18 +58,15 @@ async def init_org_balance_cache(app: FastAPI): logger.info(f"Cache populated with {len(all_orgs)} organizations") - # Close the Redis client - await redis.close() - class RedisBalanceService: def __init__( self, transaction_repo=Depends(TransactionRepository), - redis_pool: ConnectionPool = Depends(get_redis_pool), + redis_client: Redis = Depends(get_redis_client), ): self.transaction_repo = transaction_repo - self.redis_pool = redis_pool + self.redis_client = redis_client @service_handler async def populate_organization_redis_balance( @@ -92,8 +86,7 @@ async def populate_organization_redis_balance( ) ) - async with Redis(connection_pool=self.redis_pool) as redis: - await set_cache_value(organization_id, year, balance, redis) + await set_cache_value(organization_id, year, balance, self.redis_client) logger.debug( f"Set balance for org {organization_id} for {year} to {balance}" ) diff --git a/backend/lcfs/settings.py b/backend/lcfs/settings.py index 5116b5aa6..199ba941d 100644 --- a/backend/lcfs/settings.py +++ b/backend/lcfs/settings.py @@ -30,10 +30,12 @@ class Settings(BaseSettings): host: str = "0.0.0.0" port: int = 8000 - # quantity of workers for uvicorn + # Number of Uvicorn workers workers_count: int = 2 - # Enable uvicorn reloading - reload: bool = True + # Enable Uvicorn reload (True for development, False for production) + reload: bool = False + # App timeout matching OpenShift's ROUTER_DEFAULT_SERVER_TIMEOUT + timeout_keep_alive: int = 30 # Current environment environment: str = "dev" diff --git a/backend/lcfs/tests/conftest.py b/backend/lcfs/tests/conftest.py index 14f4f3e7d..5cdd52ce1 100644 --- a/backend/lcfs/tests/conftest.py +++ b/backend/lcfs/tests/conftest.py @@ -2,6 +2,7 @@ from lcfs.db.models.user.Role import RoleEnum from lcfs.web.api.base import PaginationRequestSchema, FilterModel, SortOrder +from fakeredis.aioredis import FakeRedis @pytest.fixture @@ -54,3 +55,15 @@ def role_names(self): return self.role_names return MockUserProfile() + + +@pytest.fixture +async def redis_client(): + """ + Fixture to provide a fake Redis client for tests. + """ + client = FakeRedis() + try: + yield client + finally: + await client.close() diff --git a/backend/lcfs/tests/services/redis/test_redis.py b/backend/lcfs/tests/services/redis/test_redis.py index e10906b8d..af520e7b4 100644 --- a/backend/lcfs/tests/services/redis/test_redis.py +++ b/backend/lcfs/tests/services/redis/test_redis.py @@ -1,63 +1,64 @@ -import uuid - import pytest +from unittest.mock import AsyncMock, patch from fastapi import FastAPI -from httpx import AsyncClient -from redis.asyncio import ConnectionPool, Redis -from starlette import status +from redis.exceptions import RedisError +from lcfs.services.redis.lifetime import init_redis, shutdown_redis @pytest.mark.anyio -async def test_setting_value( - fastapi_app: FastAPI, - fake_redis_pool: ConnectionPool, - client: AsyncClient, -) -> None: +async def test_init_redis_success(): + """ + Test Redis initialization succeeds and pings the client. """ - Tests that you can set value in redis. + app = FastAPI() + mock_redis = AsyncMock() + + with patch("lcfs.services.redis.lifetime.Redis", return_value=mock_redis): + # Mock Redis ping to simulate successful connection + mock_redis.ping.return_value = True + + await init_redis(app) + + assert app.state.redis_client is mock_redis + mock_redis.ping.assert_called_once() + mock_redis.close.assert_not_called() - :param fastapi_app: current application fixture. - :param fake_redis_pool: fake redis pool. - :param client: client fixture. + +@pytest.mark.anyio +async def test_init_redis_failure(): """ - url = fastapi_app.url_path_for("set_redis_value") + Test Redis initialization fails during connection. + """ + app = FastAPI() - test_key = uuid.uuid4().hex - test_val = uuid.uuid4().hex - response = await client.put( - url, - json={ - "key": test_key, - "value": test_val, - }, - ) + with patch( + "lcfs.services.redis.lifetime.Redis", + side_effect=RedisError("Connection failed"), + ): + with pytest.raises(RedisError, match="Connection failed"): + await init_redis(app) - assert response.status_code == status.HTTP_200_OK - async with Redis(connection_pool=fake_redis_pool) as redis: - actual_value = await redis.get(test_key) - assert actual_value.decode() == test_val + assert not hasattr(app.state, "redis_client") @pytest.mark.anyio -async def test_getting_value( - fastapi_app: FastAPI, - fake_redis_pool: ConnectionPool, - client: AsyncClient, -) -> None: - """ - Tests that you can get value from redis by key. - - :param fastapi_app: current application fixture. - :param fake_redis_pool: fake redis pool. - :param client: client fixture. - """ - test_key = uuid.uuid4().hex - test_val = uuid.uuid4().hex - async with Redis(connection_pool=fake_redis_pool) as redis: - await redis.set(test_key, test_val) - url = fastapi_app.url_path_for("get_redis_value") - response = await client.get(url, params={"key": test_key}) - - assert response.status_code == status.HTTP_200_OK - assert response.json()["key"] == test_key - assert response.json()["value"] == test_val +async def test_shutdown_redis_success(): + """ + Test Redis client shutdown succeeds. + """ + app = FastAPI() + mock_redis = AsyncMock() + app.state.redis_client = mock_redis + + await shutdown_redis(app) + + mock_redis.close.assert_called_once() + + +@pytest.mark.anyio +async def test_shutdown_redis_no_client(): + """ + Test Redis shutdown when no client exists. + """ + app = FastAPI() + await shutdown_redis(app) # Should not raise any exceptions diff --git a/backend/lcfs/tests/services/tfrs/test_redis_balance.py b/backend/lcfs/tests/services/tfrs/test_redis_balance.py index 9df7f20fc..39504a5f1 100644 --- a/backend/lcfs/tests/services/tfrs/test_redis_balance.py +++ b/backend/lcfs/tests/services/tfrs/test_redis_balance.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch, MagicMock, call from datetime import datetime -from redis.asyncio import ConnectionPool, Redis +from redis.asyncio import Redis from lcfs.services.tfrs.redis_balance import ( init_org_balance_cache, @@ -13,16 +13,15 @@ @pytest.mark.anyio async def test_init_org_balance_cache(): - # Mock the Redis connection pool - mock_redis_pool = AsyncMock() + # Mock the Redis client mock_redis = AsyncMock() mock_redis.set = AsyncMock() - # Ensure the `Redis` instance is created with the connection pool + # Patch Redis client creation with patch("lcfs.services.tfrs.redis_balance.Redis", return_value=mock_redis): # Mock the app object mock_app = MagicMock() - mock_app.state.redis_pool = mock_redis_pool + mock_app.state.redis_client = mock_redis current_year = datetime.now().year last_year = current_year - 1 @@ -60,9 +59,7 @@ async def test_init_org_balance_cache(): @pytest.mark.anyio -async def test_populate_organization_redis_balance( - fake_redis_pool: ConnectionPool, -): +async def test_populate_organization_redis_balance(redis_client: Redis): # Mock the transaction repository current_year = datetime.now().year last_year = current_year - 1 @@ -77,7 +74,7 @@ async def test_populate_organization_redis_balance( # Create an instance of the service with mocked dependencies service = RedisBalanceService( - transaction_repo=mock_transaction_repo, redis_pool=fake_redis_pool + transaction_repo=mock_transaction_repo, redis_client=redis_client ) await service.populate_organization_redis_balance(organization_id=1) @@ -92,12 +89,8 @@ async def test_populate_organization_redis_balance( ) # Assert that the Redis set method was called with the correct parameters - async with Redis(connection_pool=fake_redis_pool) as redis: - assert int(await redis.get(f"balance_1_{last_year}")) == 100 - assert int(await redis.get(f"balance_1_{current_year}")) == 200 - - -# mock_redis.set.assert_any_call(name=f"balance_1_{current_year}", value=200) + assert int(await redis_client.get(f"balance_1_{last_year}")) == 100 + assert int(await redis_client.get(f"balance_1_{current_year}")) == 200 @pytest.mark.anyio diff --git a/backend/lcfs/tests/test_auth_middleware.py b/backend/lcfs/tests/test_auth_middleware.py index d59076107..aaae7d74a 100644 --- a/backend/lcfs/tests/test_auth_middleware.py +++ b/backend/lcfs/tests/test_auth_middleware.py @@ -11,7 +11,7 @@ @pytest.fixture -def redis_pool(): +def redis_client(): return AsyncMock() @@ -30,14 +30,16 @@ def settings(): @pytest.fixture -def auth_backend(redis_pool, session_generator, settings): - return UserAuthentication(redis_pool, session_generator[0], settings) +def auth_backend(redis_client, session_generator, settings): + return UserAuthentication(redis_client, session_generator[0], settings) @pytest.mark.anyio async def test_load_jwk_from_redis(auth_backend): - # Mock auth_backend.redis_pool.get to return a JSON string directly - with patch.object(auth_backend.redis_pool, "get", new_callable=AsyncMock) as mock_redis_get: + # Mock auth_backend.redis_client.get to return a JSON string directly + with patch.object( + auth_backend.redis_client, "get", new_callable=AsyncMock + ) as mock_redis_get: mock_redis_get.return_value = '{"jwks": "jwks", "jwks_uri": "jwks_uri"}' await auth_backend.refresh_jwk() @@ -67,13 +69,14 @@ async def test_refresh_jwk_sets_new_keys_in_redis(mock_get, auth_backend): mock_response_2, ] - with patch.object(auth_backend.redis_pool, "get", new_callable=AsyncMock) as mock_redis_get: + with patch.object( + auth_backend.redis_client, "get", new_callable=AsyncMock + ) as mock_redis_get: mock_redis_get.return_value = None await auth_backend.refresh_jwk() - @pytest.mark.anyio async def test_authenticate_no_auth_header(auth_backend): request = MagicMock(spec=Request) diff --git a/backend/lcfs/web/api/redis/__init__.py b/backend/lcfs/web/api/redis/__init__.py deleted file mode 100644 index 24f6cdcc0..000000000 --- a/backend/lcfs/web/api/redis/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Redis API.""" - -from lcfs.web.api.redis.views import router - -__all__ = ["router"] diff --git a/backend/lcfs/web/api/redis/schema.py b/backend/lcfs/web/api/redis/schema.py deleted file mode 100644 index d4ac74f4a..000000000 --- a/backend/lcfs/web/api/redis/schema.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional - -from lcfs.web.api.base import BaseSchema - - -class RedisValueDTO(BaseSchema): - """DTO for redis values.""" - - key: str - value: Optional[str] = None # noqa: WPS110 diff --git a/backend/lcfs/web/api/redis/views.py b/backend/lcfs/web/api/redis/views.py deleted file mode 100644 index 5760d75dc..000000000 --- a/backend/lcfs/web/api/redis/views.py +++ /dev/null @@ -1,44 +0,0 @@ -from fastapi import APIRouter -from fastapi.param_functions import Depends -from redis.asyncio import ConnectionPool, Redis - -from lcfs.services.redis.dependency import get_redis_pool -from lcfs.web.api.redis.schema import RedisValueDTO - -router = APIRouter() - - -@router.get("/", response_model=RedisValueDTO) -async def get_redis_value( - key: str, - redis_pool: ConnectionPool = Depends(get_redis_pool), -) -> RedisValueDTO: - """ - Get value from redis. - - :param key: redis key, to get data from. - :param redis_pool: redis connection pool. - :returns: information from redis. - """ - async with Redis(connection_pool=redis_pool) as redis: - redis_value = await redis.get(key) - return RedisValueDTO( - key=key, - value=redis_value, - ) - - -@router.put("/") -async def set_redis_value( - redis_value: RedisValueDTO, - redis_pool: ConnectionPool = Depends(get_redis_pool), -) -> None: - """ - Set value in redis. - - :param redis_value: new value data. - :param redis_pool: redis connection pool. - """ - if redis_value.value is not None: - async with Redis(connection_pool=redis_pool) as redis: - await redis.set(name=redis_value.key, value=redis_value.value) diff --git a/backend/lcfs/web/api/router.py b/backend/lcfs/web/api/router.py index 8ac4ba401..0ae1659cc 100644 --- a/backend/lcfs/web/api/router.py +++ b/backend/lcfs/web/api/router.py @@ -4,7 +4,6 @@ echo, fuel_supply, monitoring, - redis, user, role, notification, @@ -41,7 +40,6 @@ ) api_router.include_router(transfer.router, prefix="/transfers", tags=["transfers"]) api_router.include_router(echo.router, prefix="/echo", tags=["echo"]) -api_router.include_router(redis.router, prefix="/redis", tags=["redis"]) api_router.include_router(user.router, prefix="/users", tags=["users"]) api_router.include_router(role.router, prefix="/roles", tags=["roles"]) api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"]) diff --git a/backend/lcfs/web/application.py b/backend/lcfs/web/application.py index 4a855937c..6d31484d0 100644 --- a/backend/lcfs/web/application.py +++ b/backend/lcfs/web/application.py @@ -68,13 +68,15 @@ async def authenticate(self, request): return AuthCredentials([]), UnauthenticatedUser() # Lazily retrieve Redis, session, and settings from app state - redis_pool = self.app.state.redis_pool + redis_client = self.app.state.redis_client session_factory = self.app.state.db_session_factory settings = self.app.state.settings # Now that we have the dependencies, we can instantiate the real backend real_backend = UserAuthentication( - redis_pool=redis_pool, session_factory=session_factory, settings=settings + redis_client=redis_client, + session_factory=session_factory, + settings=settings, ) # Call the authenticate method of the real backend diff --git a/backend/lcfs/web/lifetime.py b/backend/lcfs/web/lifetime.py index cbeb556b2..72d859b36 100644 --- a/backend/lcfs/web/lifetime.py +++ b/backend/lcfs/web/lifetime.py @@ -57,11 +57,8 @@ async def _startup() -> None: # noqa: WPS430 # Assign settings to app state for global access app.state.settings = settings - # Create a Redis client from the connection pool - redis_client = Redis(connection_pool=app.state.redis_pool) - # Initialize FastAPI cache with the Redis client - FastAPICache.init(RedisBackend(redis_client), prefix="lcfs") + FastAPICache.init(RedisBackend(app.state.redis_client), prefix="lcfs") await init_org_balance_cache(app) From eab0e12864b223d229d82c1957c641fbea8f9c6b Mon Sep 17 00:00:00 2001 From: Kuan Fan Date: Wed, 4 Dec 2024 13:42:04 -0800 Subject: [PATCH 31/38] update backend build base image --- backend/Dockerfile.openshift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile.openshift b/backend/Dockerfile.openshift index 58fb4ef1d..2206b39e2 100644 --- a/backend/Dockerfile.openshift +++ b/backend/Dockerfile.openshift @@ -1,5 +1,5 @@ # Base stage for common setup -FROM artifacts.developer.gov.bc.ca/docker-remote/python:3.11-slim-bullseye as base +FROM artifacts.developer.gov.bc.ca/docker-remote/python:3.11-bullseye as base RUN apt-get update && apt-get install -y \ gcc \ From 5482d4d6a6dfbf8b7f732913a0cecdfda183c517 Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Wed, 4 Dec 2024 13:42:25 -0800 Subject: [PATCH 32/38] chore: test updates for jwks --- backend/lcfs/tests/test_auth_middleware.py | 123 ++++++++------------- 1 file changed, 46 insertions(+), 77 deletions(-) diff --git a/backend/lcfs/tests/test_auth_middleware.py b/backend/lcfs/tests/test_auth_middleware.py index bdb77f2f7..146ccaf0c 100644 --- a/backend/lcfs/tests/test_auth_middleware.py +++ b/backend/lcfs/tests/test_auth_middleware.py @@ -39,30 +39,13 @@ def auth_backend(redis_client, session_generator, settings): @pytest.mark.anyio async def test_load_jwk_from_redis(auth_backend): # Mock auth_backend.redis_client.get to return a JSON string directly - with patch.object( - auth_backend.redis_client, "get", new_callable=AsyncMock - ) as mock_redis_get: - mock_redis_get.return_value = '{"jwks": "jwks", "jwks_uri": "jwks_uri"}' - - # Mock the async context manager (__aenter__ and __aexit__) - mock_redis.__aenter__.return_value = mock_redis - mock_redis.__aexit__.return_value = AsyncMock() - - # Initialize mock ConnectionPool - mock_redis_pool = AsyncMock(spec=ConnectionPool) - - # Patch the Redis class in the UserAuthentication module to return mock_redis - with patch("lcfs.services.keycloak.authentication.Redis", return_value=mock_redis): - # Initialize UserAuthentication with the mocked ConnectionPool - auth_backend = UserAuthentication( - redis_pool=mock_redis_pool, - session_factory=AsyncMock(), - settings=MagicMock( - well_known_endpoint="https://example.com/.well-known/openid-configuration" - ), - ) - - # Call refresh_jwk + mock_redis = AsyncMock() + mock_redis.get = AsyncMock( + return_value='{"jwks": "jwks_data", "jwks_uri": "jwks_uri_data"}' + ) + + # Patch Redis client in the auth backend + with patch.object(auth_backend, "redis_client", mock_redis): await auth_backend.refresh_jwk() # Assertions to verify JWKS data was loaded correctly @@ -75,7 +58,7 @@ async def test_load_jwk_from_redis(auth_backend): @pytest.mark.anyio @patch("httpx.AsyncClient.get") -async def test_refresh_jwk_sets_new_keys_in_redis(mock_httpx_get): +async def test_refresh_jwk_sets_new_keys_in_redis(mock_httpx_get, redis_client): # Mock responses for the well-known endpoint and JWKS URI mock_oidc_response = MagicMock() mock_oidc_response.json.return_value = {"jwks_uri": "https://example.com/jwks"} @@ -90,61 +73,47 @@ async def test_refresh_jwk_sets_new_keys_in_redis(mock_httpx_get): # Configure the mock to return the above responses in order mock_httpx_get.side_effect = [mock_oidc_response, mock_certs_response] - # Initialize mock Redis client - mock_redis = AsyncMock(spec=Redis) - mock_redis.get = AsyncMock(return_value=None) # JWKS data not in cache - mock_redis.set = AsyncMock() - - # Mock the async context manager (__aenter__ and __aexit__) - mock_redis.__aenter__.return_value = mock_redis - mock_redis.__aexit__.return_value = AsyncMock() - - with patch.object( - auth_backend.redis_client, "get", new_callable=AsyncMock - ) as mock_redis_get: - mock_redis_get.return_value = None - - # Patch the Redis class in the UserAuthentication module to return mock_redis - with patch("lcfs.services.keycloak.authentication.Redis", return_value=mock_redis): - # Initialize UserAuthentication with the mocked ConnectionPool - auth_backend = UserAuthentication( - redis_pool=mock_redis_pool, - session_factory=AsyncMock(), - settings=MagicMock( - well_known_endpoint="https://example.com/.well-known/openid-configuration" - ), - ) - - # Call refresh_jwk - await auth_backend.refresh_jwk() + # Mock Redis client behavior + redis_client.get = AsyncMock(return_value=None) # JWKS data not in cache + redis_client.set = AsyncMock() + + # Create auth_backend with the mocked Redis client + auth_backend = UserAuthentication( + redis_client=redis_client, + session_factory=AsyncMock(), + settings=MagicMock( + well_known_endpoint="https://example.com/.well-known/openid-configuration" + ), + ) - # Assertions to verify JWKS data was fetched and set correctly - expected_jwks = { - "keys": [ - {"kty": "RSA", "kid": "key2", "use": "sig", "n": "def", "e": "AQAB"} - ] - } - assert auth_backend.jwks == expected_jwks - assert auth_backend.jwks_uri == "https://example.com/jwks" + # Call refresh_jwk + await auth_backend.refresh_jwk() - # Verify that Redis `get` was called with "jwks_data" - mock_redis.get.assert_awaited_once_with("jwks_data") + # Assertions to verify JWKS data was fetched and set correctly + expected_jwks = { + "keys": [{"kty": "RSA", "kid": "key2", "use": "sig", "n": "def", "e": "AQAB"}] + } + assert auth_backend.jwks == expected_jwks + assert auth_backend.jwks_uri == "https://example.com/jwks" + + # Verify that Redis `get` was called with "jwks_data" + redis_client.get.assert_awaited_once_with("jwks_data") + + # Verify that the well-known endpoint was called twice + assert mock_httpx_get.call_count == 2 + mock_httpx_get.assert_any_call( + "https://example.com/.well-known/openid-configuration" + ) + mock_httpx_get.assert_any_call("https://example.com/jwks") - # Verify that the well-known endpoint was called twice - assert mock_httpx_get.call_count == 2 - mock_httpx_get.assert_any_call( - "https://example.com/.well-known/openid-configuration" - ) - mock_httpx_get.assert_any_call("https://example.com/jwks") - - # Verify that Redis `set` was called with the correct parameters - expected_jwks_data = { - "jwks": expected_jwks, - "jwks_uri": "https://example.com/jwks", - } - mock_redis.set.assert_awaited_once_with( - "jwks_data", json.dumps(expected_jwks_data), ex=86400 - ) + # Verify that Redis `set` was called with the correct parameters + expected_jwks_data = { + "jwks": expected_jwks, + "jwks_uri": "https://example.com/jwks", + } + redis_client.set.assert_awaited_once_with( + "jwks_data", json.dumps(expected_jwks_data), ex=86400 + ) @pytest.mark.anyio From ac90d9492683038cfb20f4357ef781e900cd41d9 Mon Sep 17 00:00:00 2001 From: Kuan Fan Date: Wed, 4 Dec 2024 13:55:00 -0800 Subject: [PATCH 33/38] install procps on backend image --- backend/Dockerfile.openshift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/Dockerfile.openshift b/backend/Dockerfile.openshift index 2206b39e2..2b4b4a1f4 100644 --- a/backend/Dockerfile.openshift +++ b/backend/Dockerfile.openshift @@ -1,7 +1,7 @@ # Base stage for common setup FROM artifacts.developer.gov.bc.ca/docker-remote/python:3.11-bullseye as base -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends procps \ gcc \ && rm -rf /var/lib/apt/lists/* @@ -23,9 +23,9 @@ ENV POETRY_CACHE_DIR=/.cache/pypoetry RUN poetry install --only main # Removing gcc -RUN apt-get purge -y \ - gcc \ - && rm -rf /var/lib/apt/lists/* +# RUN apt-get purge -y \ +# gcc \ +# && rm -rf /var/lib/apt/lists/* # Copying the actual application, wait-for-it script, and prestart script COPY . /app/ From d57a70a0c30a9a6a3c148a8e27b69c653c37c20c Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 4 Dec 2024 14:49:27 -0800 Subject: [PATCH 34/38] feat: Add front end feature flags * Update config with new feature flags * Add new helper function for checking if one is enabled * Follow the withRole pattern to add a withFeatureFlag HOC --- .gitignore | 1 + frontend/public/config/config.js | 4 + frontend/src/constants/config.js | 14 ++ .../utils/__tests__/withFeatureFlag.test.jsx | 109 ++++++++++++++ .../src/utils/__tests__/withRole.test.jsx | 138 +++++++++++++++++- frontend/src/utils/withFeatureFlag.jsx | 26 ++++ .../components/AssessmentCard.jsx | 56 +++---- .../src/views/Notifications/Notifications.jsx | 13 +- 8 files changed, 332 insertions(+), 29 deletions(-) create mode 100644 frontend/src/utils/__tests__/withFeatureFlag.test.jsx create mode 100644 frontend/src/utils/withFeatureFlag.jsx diff --git a/.gitignore b/.gitignore index 81b02a92e..e437ac444 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ *.py[cod] *$py.class docs/ +.DS_Store # C extensions *.so diff --git a/frontend/public/config/config.js b/frontend/public/config/config.js index 01cbed624..a53256ce3 100644 --- a/frontend/public/config/config.js +++ b/frontend/public/config/config.js @@ -7,6 +7,10 @@ export const config = { POST_LOGOUT_URL: 'http://localhost:3000/', SM_LOGOUT_URL: 'https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=' + }, + feature_flags: { + supplementalReporting: true, + notifications: false } } diff --git a/frontend/src/constants/config.js b/frontend/src/constants/config.js index 2763bbcf2..8dfec5e6c 100644 --- a/frontend/src/constants/config.js +++ b/frontend/src/constants/config.js @@ -28,6 +28,15 @@ export function getApiBaseUrl() { return window.lcfs_config.api_base ?? baseUrl } +export const isFeatureEnabled = (featureFlag) => { + return CONFIG.feature_flags[featureFlag] +} + +export const FEATURE_FLAGS = { + SUPPLEMENTAL_REPORTING: 'supplementalReporting', + NOTIFICATIONS: 'notifications' +} + export const CONFIG = { API_BASE: getApiBaseUrl(), KEYCLOAK: { @@ -42,5 +51,10 @@ export const CONFIG = { SM_LOGOUT_URL: window.lcfs_config.keycloak.SM_LOGOUT_URL ?? 'https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=' + }, + feature_flags: { + supplementalReporting: + window.lcfs_config.feature_flags.supplementalReporting ?? true, + notifications: window.lcfs_config.feature_flags.notifications ?? false } } diff --git a/frontend/src/utils/__tests__/withFeatureFlag.test.jsx b/frontend/src/utils/__tests__/withFeatureFlag.test.jsx new file mode 100644 index 000000000..b619a1f25 --- /dev/null +++ b/frontend/src/utils/__tests__/withFeatureFlag.test.jsx @@ -0,0 +1,109 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import withFeatureFlag from '../withFeatureFlag.jsx' // Adjust the import path as necessary +import { isFeatureEnabled } from '@/constants/config.js' + +// Mock the isFeatureEnabled function +vi.mock('@/constants/config.js', () => ({ + isFeatureEnabled: vi.fn() +})) + +// Mock Navigate component +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + Navigate: ({ to }) =>
Navigate to {to}
+})) + +// Define a mock component to be wrapped +const MockComponent = () =>
Feature Enabled Content
+ +describe('withFeatureFlag HOC', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders the wrapped component when the feature flag is enabled', () => { + isFeatureEnabled.mockReturnValue(true) + + const WrappedComponent = withFeatureFlag( + MockComponent, + 'new-feature', + '/fallback' + ) + + render() + + expect(screen.getByText('Feature Enabled Content')).toBeInTheDocument() + }) + + it('redirects to the specified path when the feature flag is disabled and redirect is provided', () => { + isFeatureEnabled.mockReturnValue(false) + + const WrappedComponent = withFeatureFlag( + MockComponent, + 'new-feature', + '/fallback' + ) + + render() + + const navigateElement = screen.getByTestId('navigate') + expect(navigateElement).toBeInTheDocument() + expect(navigateElement).toHaveTextContent('Navigate to /fallback') + }) + + it('renders null when the feature flag is disabled and no redirect is provided', () => { + isFeatureEnabled.mockReturnValue(false) + + const WrappedComponent = withFeatureFlag(MockComponent, 'new-feature') + + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('sets the correct display name for the wrapped component', () => { + isFeatureEnabled.mockReturnValue(true) + + const WrappedComponent = withFeatureFlag( + MockComponent, + 'new-feature', + '/fallback' + ) + + render() + + expect(WrappedComponent.displayName).toBe('WithFeatureFlag(MockComponent)') + }) + + it('handles undefined featureFlag gracefully by rendering the wrapped component', () => { + isFeatureEnabled.mockReturnValue(false) + + const WrappedComponent = withFeatureFlag( + MockComponent, + undefined, + '/fallback' + ) + + render() + + const navigateElement = screen.getByTestId('navigate') + expect(navigateElement).toBeInTheDocument() + expect(navigateElement).toHaveTextContent('Navigate to /fallback') + }) + + it('handles null props correctly by passing them to the wrapped component', () => { + isFeatureEnabled.mockReturnValue(true) + + const WrappedComponent = withFeatureFlag( + MockComponent, + 'new-feature', + '/fallback' + ) + + render() + + expect(screen.getByText('Feature Enabled Content')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/utils/__tests__/withRole.test.jsx b/frontend/src/utils/__tests__/withRole.test.jsx index 1d74ce903..350efd3ca 100644 --- a/frontend/src/utils/__tests__/withRole.test.jsx +++ b/frontend/src/utils/__tests__/withRole.test.jsx @@ -1 +1,137 @@ -describe.todo() +import React from 'react' +import { render, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import withRole from '../withRole.jsx' +import { useCurrentUser } from '@/hooks/useCurrentUser' + +// Mock the useCurrentUser hook +vi.mock('@/hooks/useCurrentUser') + +// Mock Navigate component +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + Navigate: ({ to }) =>
Navigate to {to}
+})) + +// Define a mock component to be wrapped +const MockComponent = () =>
Protected Content
+ +describe('withRole HOC', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders Loading... when currentUser is undefined', () => { + useCurrentUser.mockReturnValue({ + data: undefined + }) + + const WrappedComponent = withRole( + MockComponent, + ['admin', 'user'], + '/login' + ) + + render() + + expect(screen.getByText('Loading...')).toBeInTheDocument() + }) + + it('renders the wrapped component when user has an allowed role', () => { + useCurrentUser.mockReturnValue({ + data: { + roles: [{ name: 'user' }, { name: 'editor' }] + } + }) + + const WrappedComponent = withRole( + MockComponent, + ['admin', 'user'], + '/login' + ) + + render() + + expect(screen.getByText('Protected Content')).toBeInTheDocument() + }) + + it('redirects to the specified path when user does not have an allowed role and redirect is provided', () => { + useCurrentUser.mockReturnValue({ + data: { + roles: [{ name: 'guest' }] + } + }) + + const WrappedComponent = withRole( + MockComponent, + ['admin', 'user'], + '/login' + ) + + render() + + const navigateElement = screen.getByTestId('navigate') + expect(navigateElement).toBeInTheDocument() + expect(navigateElement).toHaveTextContent('Navigate to /login') + }) + + it('renders null when user does not have an allowed role and no redirect is provided', () => { + useCurrentUser.mockReturnValue({ + data: { + roles: [{ name: 'guest' }] + } + }) + + const WrappedComponent = withRole(MockComponent, ['admin', 'user']) + + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('sets the correct display name for the wrapped component', () => { + useCurrentUser.mockReturnValue({ + data: { + roles: [{ name: 'admin' }] + } + }) + + const WrappedComponent = withRole(MockComponent, ['admin'], '/login') + + render() + + expect(WrappedComponent.displayName).toBe('WithRole(MockComponent)') + }) + + it('handles currentUser with no roles gracefully', () => { + useCurrentUser.mockReturnValue({ + data: { + roles: [] + } + }) + + const WrappedComponent = withRole(MockComponent, ['admin'], '/login') + + render() + + const navigateElement = screen.getByTestId('navigate') + expect(navigateElement).toBeInTheDocument() + expect(navigateElement).toHaveTextContent('Navigate to /login') + }) + + it('handles currentUser.roles being undefined gracefully', () => { + useCurrentUser.mockReturnValue({ + data: { + // roles is undefined + } + }) + + const WrappedComponent = withRole(MockComponent, ['admin'], '/login') + + render() + + const navigateElement = screen.getByTestId('navigate') + expect(navigateElement).toBeInTheDocument() + expect(navigateElement).toHaveTextContent('Navigate to /login') + }) +}) diff --git a/frontend/src/utils/withFeatureFlag.jsx b/frontend/src/utils/withFeatureFlag.jsx new file mode 100644 index 000000000..55b536f17 --- /dev/null +++ b/frontend/src/utils/withFeatureFlag.jsx @@ -0,0 +1,26 @@ +import { Navigate } from 'react-router-dom' +import { isFeatureEnabled } from '@/constants/config.js' + +export const withFeatureFlag = (WrappedComponent, featureFlag, redirect) => { + const WithFeatureFlag = (props) => { + const isEnabled = isFeatureEnabled(featureFlag) + + if (!isEnabled && redirect) { + return + } + if (!isEnabled && !redirect) { + return null + } + + return + } + + // Display name for the wrapped component + WithFeatureFlag.displayName = `WithFeatureFlag(${ + WrappedComponent.displayName || WrappedComponent.name || 'Component' + })` + + return WithFeatureFlag +} + +export default withFeatureFlag diff --git a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx index 65be99cc1..339b22133 100644 --- a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx +++ b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx @@ -13,6 +13,7 @@ import { useNavigate } from 'react-router-dom' import { StyledListItem } from '@/components/StyledListItem' import { roles } from '@/constants/roles' import { Role } from '@/components/Role' +import { FEATURE_FLAGS, isFeatureEnabled } from '@/constants/config.js' export const AssessmentCard = ({ orgData, @@ -173,7 +174,7 @@ export const AssessmentCard = ({ variant="h6" color="primary" > - {t(`report:reportHistory`)} + {t('report:reportHistory')} {filteredHistory.map((item, index) => ( @@ -202,33 +203,34 @@ export const AssessmentCard = ({ )} - {currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED && ( - <> - - {t('report:supplementalWarning')} - - - { - createSupplementalReport() - }} - startIcon={} - sx={{ mt: 2 }} - disabled={isLoading} + {isFeatureEnabled(FEATURE_FLAGS.SUPPLEMENTAL_REPORTING) && + currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED && ( + <> + - {t('report:createSupplementalRptBtn')} - - - - )} + {t('report:supplementalWarning')} + + + { + createSupplementalReport() + }} + startIcon={} + sx={{ mt: 2 }} + disabled={isLoading} + > + {t('report:createSupplementalRptBtn')} + + + + )} diff --git a/frontend/src/views/Notifications/Notifications.jsx b/frontend/src/views/Notifications/Notifications.jsx index 5841ee3c7..da555141f 100644 --- a/frontend/src/views/Notifications/Notifications.jsx +++ b/frontend/src/views/Notifications/Notifications.jsx @@ -1,3 +1,14 @@ -export const Notifications = () => { +import * as ROUTES from '@/constants/routes/routes.js' +import withFeatureFlag from '@/utils/withFeatureFlag.jsx' +import { FEATURE_FLAGS } from '@/constants/config.js' + +export const NotificationsBase = () => { return
Notifications
} + +export const Notifications = withFeatureFlag( + NotificationsBase, + FEATURE_FLAGS.NOTIFICATIONS, + ROUTES.DASHBOARD +) +Notifications.displayName = 'Notifications' From 739f1d8ce6e9eeb5e6d46f319aa7bd147c71b5df Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Wed, 4 Dec 2024 15:51:22 -0800 Subject: [PATCH 35/38] feat: async s3 aioboto3 setup --- backend/lcfs/services/redis/lifetime.py | 59 ++-- backend/lcfs/services/s3/client.py | 36 ++- backend/lcfs/services/s3/dependency.py | 7 +- backend/lcfs/services/s3/lifetime.py | 13 +- backend/poetry.lock | 384 +++++++++++++++++++++++- backend/pyproject.toml | 1 + 6 files changed, 443 insertions(+), 57 deletions(-) diff --git a/backend/lcfs/services/redis/lifetime.py b/backend/lcfs/services/redis/lifetime.py index 6d4a5f2eb..3b7347ea2 100644 --- a/backend/lcfs/services/redis/lifetime.py +++ b/backend/lcfs/services/redis/lifetime.py @@ -1,8 +1,8 @@ import logging from fastapi import FastAPI from redis.asyncio import Redis -from redis.exceptions import RedisError - +from redis.exceptions import RedisError, TimeoutError +import asyncio from lcfs.settings import settings logger = logging.getLogger(__name__) @@ -14,27 +14,38 @@ async def init_redis(app: FastAPI) -> None: :param app: current FastAPI application. """ - try: - # Initialize Redis client directly - app.state.redis_client = Redis( - host=settings.redis_host, - port=settings.redis_port, - password=settings.redis_pass, - db=settings.redis_base or 0, - decode_responses=True, - socket_timeout=5, # Timeout for socket read/write (seconds) - socket_connect_timeout=5, # Timeout for connection establishment (seconds) - ) - - # Test the connection - await app.state.redis_client.ping() - logger.info("Redis client initialized and connection successful.") - except RedisError as e: - logger.error(f"Redis error during initialization: {e}") - raise - except Exception as e: - logger.error(f"Unexpected error during Redis initialization: {e}") - raise + retries = 5 # Retry logic in case Redis is unavailable initially + for i in range(retries): + try: + # Initialize Redis client + app.state.redis_client = Redis( + host=settings.redis_host, + port=settings.redis_port, + password=settings.redis_pass, + db=settings.redis_base or 0, + decode_responses=True, + max_connections=10, + socket_timeout=5, + socket_connect_timeout=5, + ) + + # Test the connection + await app.state.redis_client.ping() + logger.info("Redis client initialized and connection successful.") + break + except TimeoutError as e: + logger.error(f"Redis timeout during initialization attempt {i + 1}: {e}") + if i == retries - 1: + raise + await asyncio.sleep(2**i) # Exponential backoff + except RedisError as e: + logger.error(f"Redis error during initialization attempt {i + 1}: {e}") + if i == retries - 1: + raise + await asyncio.sleep(2**i) # Exponential backoff + except Exception as e: + logger.error(f"Unexpected error during Redis initialization: {e}") + raise async def shutdown_redis(app: FastAPI) -> None: @@ -46,7 +57,7 @@ async def shutdown_redis(app: FastAPI) -> None: try: if hasattr(app.state, "redis_client") and app.state.redis_client: await app.state.redis_client.close() - logger.info("Redis client closed successfully.") + logger.info("Redis client closed successfully.") except RedisError as e: logger.error(f"Redis error during shutdown: {e}") except Exception as e: diff --git a/backend/lcfs/services/s3/client.py b/backend/lcfs/services/s3/client.py index 11ee27397..309147971 100644 --- a/backend/lcfs/services/s3/client.py +++ b/backend/lcfs/services/s3/client.py @@ -1,8 +1,6 @@ import os import uuid - -import boto3 -from fastapi import Depends, Request +from fastapi import Depends from pydantic.v1 import ValidationError from sqlalchemy import select from sqlalchemy.exc import InvalidRequestError @@ -53,12 +51,13 @@ async def upload_file(self, file, parent_id: str, parent_type="compliance_report self.clamav_service.scan_file(file) # Upload file to S3 - self.s3_client.upload_fileobj( - Fileobj=file.file, - Bucket=BUCKET_NAME, - Key=file_key, - ExtraArgs={"ContentType": file.content_type}, - ) + async with self.s3_client as client: + await client.upload_fileobj( + Fileobj=file.file, + Bucket=BUCKET_NAME, + Key=file_key, + ExtraArgs={"ContentType": file.content_type}, + ) document = Document( file_key=file_key, @@ -97,11 +96,12 @@ async def generate_presigned_url(self, document_id: int): if not document: raise Exception("Document not found") - presigned_url = self.s3_client.generate_presigned_url( - "get_object", - Params={"Bucket": BUCKET_NAME, "Key": document.file_key}, - ExpiresIn=60, # URL expiration in seconds - ) + async with self.s3_client as client: + presigned_url = await client.generate_presigned_url( + "get_object", + Params={"Bucket": BUCKET_NAME, "Key": document.file_key}, + ExpiresIn=60, # URL expiration in seconds + ) return presigned_url # Delete a file from S3 and remove the entry from the database @@ -113,7 +113,8 @@ async def delete_file(self, document_id: int): raise Exception("Document not found") # Delete the file from S3 - self.s3_client.delete_object(Bucket=BUCKET_NAME, Key=document.file_key) + async with self.s3_client as client: + await client.delete_object(Bucket=BUCKET_NAME, Key=document.file_key) # Delete the entry from the database await self.db.delete(document) @@ -144,5 +145,8 @@ async def get_object(self, document_id: int): if not document: raise Exception("Document not found") - response = self.s3_client.get_object(Bucket=BUCKET_NAME, Key=document.file_key) + async with self.s3_client as client: + response = await client.get_object( + Bucket=BUCKET_NAME, Key=document.file_key + ) return response, document diff --git a/backend/lcfs/services/s3/dependency.py b/backend/lcfs/services/s3/dependency.py index d46027c42..4a30872f2 100644 --- a/backend/lcfs/services/s3/dependency.py +++ b/backend/lcfs/services/s3/dependency.py @@ -1,17 +1,18 @@ from starlette.requests import Request -import boto3 +from aioboto3 import Session # S3 Client Dependency async def get_s3_client( request: Request, -) -> boto3.client: +) -> Session.client: """ Returns the S3 client from the application state. Usage: >>> async def handler(s3_client = Depends(get_s3_client)): - >>> s3_client.upload_file('file.txt', 'my-bucket', 'file.txt') + >>> async with s3_client as client: + >>> await client.upload_fileobj('file.txt', 'my-bucket', 'file.txt') :param request: Current request object. :returns: S3 client. diff --git a/backend/lcfs/services/s3/lifetime.py b/backend/lcfs/services/s3/lifetime.py index 443f49eae..ce853bf3c 100644 --- a/backend/lcfs/services/s3/lifetime.py +++ b/backend/lcfs/services/s3/lifetime.py @@ -1,5 +1,5 @@ -import boto3 from fastapi import FastAPI +from aioboto3 import Session from lcfs.settings import settings @@ -9,14 +9,15 @@ async def init_s3(app: FastAPI) -> None: :param app: FastAPI application. """ - app.state.s3_client = boto3.client( + session = Session() + app.state.s3_client = session.client( "s3", aws_access_key_id=settings.s3_access_key, aws_secret_access_key=settings.s3_secret_key, endpoint_url=settings.s3_endpoint, region_name="us-east-1", ) - print("S3 client initialized.") + print("Async S3 client initialized.") async def shutdown_s3(app: FastAPI) -> None: @@ -25,6 +26,6 @@ async def shutdown_s3(app: FastAPI) -> None: :param app: FastAPI application. """ - if hasattr(app.state, "s3_client"): - del app.state.s3_client - print("S3 client shutdown.") + if hasattr(app.state, "s3_client") and app.state.s3_client: + await app.state.s3_client.close() + print("Async S3 client shutdown.") diff --git a/backend/poetry.lock b/backend/poetry.lock index d55b99abb..f63fce6e4 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -15,6 +15,185 @@ files = [ aiormq = ">=6.8.0,<6.9.0" yarl = "*" +[[package]] +name = "aioboto3" +version = "13.2.0" +description = "Async boto3 wrapper" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "aioboto3-13.2.0-py3-none-any.whl", hash = "sha256:fd894b8d319934dfd75285b58da35560670e57182d0148c54a3d4ee5da730c78"}, + {file = "aioboto3-13.2.0.tar.gz", hash = "sha256:92c3232e0bf7dcb5d921cd1eb8c5e0b856c3985f7c1cd32ab3cd51adc5c9b5da"}, +] + +[package.dependencies] +aiobotocore = {version = "2.15.2", extras = ["boto3"]} +aiofiles = ">=23.2.1" + +[package.extras] +chalice = ["chalice (>=1.24.0)"] +s3cse = ["cryptography (>=2.3.1)"] + +[[package]] +name = "aiobotocore" +version = "2.15.2" +description = "Async client for aws services using botocore and aiohttp" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiobotocore-2.15.2-py3-none-any.whl", hash = "sha256:d4d3128b4b558e2b4c369bfa963b022d7e87303adb82eec623cec8aa77ae578a"}, + {file = "aiobotocore-2.15.2.tar.gz", hash = "sha256:9ac1cfcaccccc80602968174aa032bf978abe36bd4e55e6781d6500909af1375"}, +] + +[package.dependencies] +aiohttp = ">=3.9.2,<4.0.0" +aioitertools = ">=0.5.1,<1.0.0" +boto3 = {version = ">=1.35.16,<1.35.37", optional = true, markers = "extra == \"boto3\""} +botocore = ">=1.35.16,<1.35.37" +wrapt = ">=1.10.10,<2.0.0" + +[package.extras] +awscli = ["awscli (>=1.34.16,<1.35.3)"] +boto3 = ["boto3 (>=1.35.16,<1.35.37)"] + +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.9" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0411777249f25d11bd2964a230b3ffafcbed6cd65d0f2b132bc2b8f5b8c347c7"}, + {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:499368eb904566fbdf1a3836a1532000ef1308f34a1bcbf36e6351904cced771"}, + {file = "aiohttp-3.11.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b5a5009b0159a8f707879dc102b139466d8ec6db05103ec1520394fdd8ea02c"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176f8bb8931da0613bb0ed16326d01330066bb1e172dd97e1e02b1c27383277b"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6435a66957cdba1a0b16f368bde03ce9c79c57306b39510da6ae5312a1a5b2c1"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:202f40fb686e5f93908eee0c75d1e6fbe50a43e9bd4909bf3bf4a56b560ca180"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6beeac698671baa558e82fa160be9761cf0eb25861943f4689ecf9000f8ebd0"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:96726839a42429318017e67a42cca75d4f0d5248a809b3cc2e125445edd7d50d"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3f5461c77649358610fb9694e790956b4238ac5d9e697a17f63619c096469afe"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4313f3bc901255b22f01663eeeae167468264fdae0d32c25fc631d5d6e15b502"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6e274661c74195708fc4380a4ef64298926c5a50bb10fbae3d01627d7a075b7"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db2914de2559809fdbcf3e48f41b17a493b58cb7988d3e211f6b63126c55fe82"}, + {file = "aiohttp-3.11.9-cp310-cp310-win32.whl", hash = "sha256:27935716f8d62c1c73010428db310fd10136002cfc6d52b0ba7bdfa752d26066"}, + {file = "aiohttp-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:afbe85b50ade42ddff5669947afde9e8a610e64d2c80be046d67ec4368e555fa"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:afcda759a69c6a8be3aae764ec6733155aa4a5ad9aad4f398b52ba4037942fe3"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5bba6b83fde4ca233cfda04cbd4685ab88696b0c8eaf76f7148969eab5e248a"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:442356e8924fe1a121f8c87866b0ecdc785757fd28924b17c20493961b3d6697"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f737fef6e117856400afee4f17774cdea392b28ecf058833f5eca368a18cf1bf"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea142255d4901b03f89cb6a94411ecec117786a76fc9ab043af8f51dd50b5313"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1e9e447856e9b7b3d38e1316ae9a8c92e7536ef48373de758ea055edfd5db5"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f6173302f8a329ca5d1ee592af9e628d3ade87816e9958dcf7cdae2841def7"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c6147c6306f537cff59409609508a1d2eff81199f0302dd456bb9e7ea50c39"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9d036a9a41fc78e8a3f10a86c2fc1098fca8fab8715ba9eb999ce4788d35df0"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2ac9fd83096df36728da8e2f4488ac3b5602238f602706606f3702f07a13a409"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3108f0ad5c6b6d78eec5273219a5bbd884b4aacec17883ceefaac988850ce6e"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:96bbec47beb131bbf4bae05d8ef99ad9e5738f12717cfbbf16648b78b0232e87"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc726c3fa8f606d07bd2b500e5dc4c0fd664c59be7788a16b9e34352c50b6b6b"}, + {file = "aiohttp-3.11.9-cp311-cp311-win32.whl", hash = "sha256:5720ebbc7a1b46c33a42d489d25d36c64c419f52159485e55589fbec648ea49a"}, + {file = "aiohttp-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:17af09d963fa1acd7e4c280e9354aeafd9e3d47eaa4a6bfbd2171ad7da49f0c5"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1f2d7fd583fc79c240094b3e7237d88493814d4b300d013a42726c35a734bc9"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4b8a1b6c7a68c73191f2ebd3bf66f7ce02f9c374e309bdb68ba886bbbf1b938"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd3f711f4c99da0091ced41dccdc1bcf8be0281dc314d6d9c6b6cf5df66f37a9"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cb1a1326a0264480a789e6100dc3e07122eb8cd1ad6b784a3d47d13ed1d89c"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a7ddf981a0b953ade1c2379052d47ccda2f58ab678fca0671c7c7ca2f67aac2"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ffa45cc55b18d4ac1396d1ddb029f139b1d3480f1594130e62bceadf2e1a838"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cca505829cdab58c2495ff418c96092d225a1bbd486f79017f6de915580d3c44"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44d323aa80a867cb6db6bebb4bbec677c6478e38128847f2c6b0f70eae984d72"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2fab23003c4bb2249729a7290a76c1dda38c438300fdf97d4e42bf78b19c810"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:be0c7c98e38a1e3ad7a6ff64af8b6d6db34bf5a41b1478e24c3c74d9e7f8ed42"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cc5e0d069c56645446c45a4b5010d4b33ac6c5ebfd369a791b5f097e46a3c08"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9bcf97b971289be69638d8b1b616f7e557e1342debc7fc86cf89d3f08960e411"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c7333e7239415076d1418dbfb7fa4df48f3a5b00f8fdf854fca549080455bc14"}, + {file = "aiohttp-3.11.9-cp312-cp312-win32.whl", hash = "sha256:9384b07cfd3045b37b05ed002d1c255db02fb96506ad65f0f9b776b762a7572e"}, + {file = "aiohttp-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:f5252ba8b43906f206048fa569debf2cd0da0316e8d5b4d25abe53307f573941"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:282e0a7ddd36ebc411f156aeaa0491e8fe7f030e2a95da532cf0c84b0b70bc66"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd3e6b0c7d4954cca59d241970011f8d3327633d555051c430bd09ff49dc494"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30f9f89ae625d412043f12ca3771b2ccec227cc93b93bb1f994db6e1af40a7d3"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3b5b2c012d70c63d9d13c57ed1603709a4d9d7d473e4a9dfece0e4ea3d5f51"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ef1550bb5f55f71b97a6a395286db07f7f2c01c8890e613556df9a51da91e8d"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317251b9c9a2f1a9ff9cd093775b34c6861d1d7df9439ce3d32a88c275c995cd"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cbe97839b009826a61b143d3ca4964c8590d7aed33d6118125e5b71691ca46"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:618b18c3a2360ac940a5503da14fa4f880c5b9bc315ec20a830357bcc62e6bae"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0cf4d814689e58f57ecd5d8c523e6538417ca2e72ff52c007c64065cef50fb2"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:15c4e489942d987d5dac0ba39e5772dcbed4cc9ae3710d1025d5ba95e4a5349c"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8df0ff5a911c6d21957a9182402aad7bf060eaeffd77c9ea1c16aecab5adbf"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ed95d66745f53e129e935ad726167d3a6cb18c5d33df3165974d54742c373868"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:647ec5bee7e4ec9f1034ab48173b5fa970d9a991e565549b965e93331f1328fe"}, + {file = "aiohttp-3.11.9-cp313-cp313-win32.whl", hash = "sha256:ef2c9499b7bd1e24e473dc1a85de55d72fd084eea3d8bdeec7ee0720decb54fa"}, + {file = "aiohttp-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:84de955314aa5e8d469b00b14d6d714b008087a0222b0f743e7ffac34ef56aff"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e738aabff3586091221044b7a584865ddc4d6120346d12e28e788307cd731043"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28f29bce89c3b401a53d6fd4bee401ee943083bf2bdc12ef297c1d63155070b0"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31de2f10f63f96cc19e04bd2df9549559beadd0b2ee2da24a17e7ed877ca8c60"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f31cebd8c27a36af6c7346055ac564946e562080ee1a838da724585c67474f"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bcb7f6976dc0b6b56efde13294862adf68dd48854111b422a336fa729a82ea6"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8b13b9950d8b2f8f58b6e5842c4b842b5887e2c32e3f4644d6642f1659a530"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c23e62f3545c2216100603614f9e019e41b9403c47dd85b8e7e5015bf1bde0"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec656680fc53a13f849c71afd0c84a55c536206d524cbc831cde80abbe80489e"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:36df00e0541f264ce42d62280281541a47474dfda500bc5b7f24f70a7f87be7a"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8dcfd14c712aa9dd18049280bfb2f95700ff6a8bde645e09f17c3ed3f05a0130"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14624d96f0d69cf451deed3173079a68c322279be6030208b045ab77e1e8d550"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4b01d9cfcb616eeb6d40f02e66bebfe7b06d9f2ef81641fdd50b8dd981166e0b"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:928f92f80e2e8d6567b87d3316c1fd9860ccfe36e87a9a7f5237d4cda8baa1ba"}, + {file = "aiohttp-3.11.9-cp39-cp39-win32.whl", hash = "sha256:c8a02f74ae419e3955af60f570d83187423e42e672a6433c5e292f1d23619269"}, + {file = "aiohttp-3.11.9-cp39-cp39-win_amd64.whl", hash = "sha256:0a97d657f6cf8782a830bb476c13f7d777cfcab8428ac49dde15c22babceb361"}, + {file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aioitertools" +version = "0.12.0" +description = "itertools and builtins for AsyncIO and mixed iterables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796"}, + {file = "aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b"}, +] + +[package.dependencies] +typing_extensions = {version = ">=4.0", markers = "python_version < \"3.10\""} + +[package.extras] +dev = ["attribution (==1.8.0)", "black (==24.8.0)", "build (>=1.2)", "coverage (==7.6.1)", "flake8 (==7.1.1)", "flit (==3.9.0)", "mypy (==1.11.2)", "ufmt (==2.7.1)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==8.0.2)", "sphinx-mdinclude (==0.6.2)"] + [[package]] name = "aiormq" version = "6.8.1" @@ -30,6 +209,20 @@ files = [ pamqp = "3.3.0" yarl = "*" +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + [[package]] name = "alembic" version = "1.14.0" @@ -274,17 +467,17 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.35.64" +version = "1.35.36" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.64-py3-none-any.whl", hash = "sha256:cdacf03fc750caa3aa0dbf6158166def9922c9d67b4160999ff8fc350662facc"}, - {file = "boto3-1.35.64.tar.gz", hash = "sha256:bc3fc12b41fa2c91e51ab140f74fb1544408a2b1e00f88a4c2369a66d18ddf20"}, + {file = "boto3-1.35.36-py3-none-any.whl", hash = "sha256:33735b9449cd2ef176531ba2cb2265c904a91244440b0e161a17da9d24a1e6d1"}, + {file = "boto3-1.35.36.tar.gz", hash = "sha256:586524b623e4fbbebe28b604c6205eb12f263cc4746bccb011562d07e217a4cb"}, ] [package.dependencies] -botocore = ">=1.35.64,<1.36.0" +botocore = ">=1.35.36,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -293,13 +486,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.64" +version = "1.35.36" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.64-py3-none-any.whl", hash = "sha256:bbd96bf7f442b1d5e35b36f501076e4a588c83d8d84a1952e9ee1d767e5efb3e"}, - {file = "botocore-1.35.64.tar.gz", hash = "sha256:2f95c83f31c9e38a66995c88810fc638c829790e125032ba00ab081a2cf48cb9"}, + {file = "botocore-1.35.36-py3-none-any.whl", hash = "sha256:64241c778bf2dc863d93abab159e14024d97a926a5715056ef6411418cb9ead3"}, + {file = "botocore-1.35.36.tar.gz", hash = "sha256:354ec1b766f0029b5d6ff0c45d1a0f9e5007b7d2f3ec89bcdd755b208c5bc797"}, ] [package.dependencies] @@ -1132,6 +1325,107 @@ files = [ [package.dependencies] flake8 = "*" +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + [[package]] name = "greenlet" version = "3.1.1" @@ -3658,6 +3952,80 @@ pep8-naming = ">=0.13,<0.14" pygments = ">=2.4,<3.0" typing_extensions = ">=4.0,<5.0" +[[package]] +name = "wrapt" +version = "1.17.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +files = [ + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, + {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, + {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, + {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, + {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, + {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, +] + [[package]] name = "xlrd" version = "2.0.1" @@ -3784,4 +4152,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "6ac5bbf72db0f71b3519fd274af81ebfa064cad93bdf34c801568939d6e4214f" +content-hash = "4fd345bed13bd10e81d85a185b72c1ff3cbbb006734b6f812ea144976a2bf0ec" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 120fe80d5..45f128340 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -46,6 +46,7 @@ python-multipart = "^0.0.16" aio-pika = "^9.4.3" jinja2 = "^3.1.4" requests = "^2.32.3" +aioboto3 = "^13.2.0" [tool.poetry.dev-dependencies] pytest = "^8.3.3" From 42df654dcd872b9262933928d1c850985ba101cb Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 4 Dec 2024 15:14:05 -0800 Subject: [PATCH 36/38] fix: Data and Label Fixes * Change hydrogen to kg * Relabel allocation agreement transaciton types * Rename first column to Responsibility * Add $ infront of CAD --- .../versions/2024-12-04-23-00_8491890dd688.py | 57 +++++++++++++++++++ .../common/allocation_agreement_seeder.py | 8 +-- .../db/seeders/common/seed_fuel_data.json | 3 +- .../locales/en/allocationAgreement.json | 2 +- frontend/src/assets/locales/en/transfer.json | 2 +- 5 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 backend/lcfs/db/migrations/versions/2024-12-04-23-00_8491890dd688.py diff --git a/backend/lcfs/db/migrations/versions/2024-12-04-23-00_8491890dd688.py b/backend/lcfs/db/migrations/versions/2024-12-04-23-00_8491890dd688.py new file mode 100644 index 000000000..d12c0a57a --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2024-12-04-23-00_8491890dd688.py @@ -0,0 +1,57 @@ +"""Data Fixes + +Revision ID: 8491890dd688 +Revises: aeaa26f5cdd5 +Create Date: 2024-12-04 23:00:10.708533 + +""" + +from alembic import op +from sqlalchemy import update + +from lcfs.db.models import FuelType, AllocationTransactionType +from lcfs.db.models.fuel.FuelType import QuantityUnitsEnum + +# revision identifiers, used by Alembic. +revision = "8491890dd688" +down_revision = "aeaa26f5cdd5" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute( + update(FuelType) + .where(FuelType.fuel_type_id == 6) + .values(units=QuantityUnitsEnum.Kilograms) + ) + + op.execute( + update(FuelType).where(FuelType.fuel_type_id == 20).values(fossil_derived=False) + ) + + # Update 'type' and 'description' in allocation_transaction_type where allocation_transaction_type_id = 2 + op.execute( + update(AllocationTransactionType) + .where(AllocationTransactionType.allocation_transaction_type_id == 2) + .values( + type="Allocated to", + description="Fuel allocated to another supplier under an allocation agreement", + ) + ) + + # Update 'type' and 'description' in allocation_transaction_type where allocation_transaction_type_id = 1 + op.execute( + update(AllocationTransactionType) + .where(AllocationTransactionType.allocation_transaction_type_id == 1) + .values( + type="Allocated from", + description="Fuel allocated from another supplier under an allocation agreement", + ) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + pass diff --git a/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py b/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py index e78fc2bbc..e48a57227 100644 --- a/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py +++ b/backend/lcfs/db/seeders/common/allocation_agreement_seeder.py @@ -18,15 +18,15 @@ async def seed_allocation_transaction_types(session): allocation_transaction_types_to_seed = [ { "allocation_transaction_type_id": 1, - "type": "Purchased", - "description": "Fuel purchased under an allocation agreement", + "type": "Allocated from", + "description": "Fuel allocated from another supplier under an allocation agreement", "display_order": 1, "effective_date": datetime.strptime("2012-01-01", "%Y-%m-%d").date(), }, { "allocation_transaction_type_id": 2, - "type": "Sold", - "description": "Fuel sold under an allocation agreement", + "type": "Allocated to", + "description": "Fuel allocated to another supplier under an allocation agreement", "display_order": 2, "effective_date": datetime.strptime("2012-01-01", "%Y-%m-%d").date(), }, diff --git a/backend/lcfs/db/seeders/common/seed_fuel_data.json b/backend/lcfs/db/seeders/common/seed_fuel_data.json index 828d7576d..db08313f8 100644 --- a/backend/lcfs/db/seeders/common/seed_fuel_data.json +++ b/backend/lcfs/db/seeders/common/seed_fuel_data.json @@ -83,7 +83,7 @@ "provision_1_id": 2, "provision_2_id": 3, "default_carbon_intensity": 123.96, - "units": "kWh", + "units": "kg", "unrecognized": false }, { @@ -97,7 +97,6 @@ "units": "kg", "unrecognized": false }, - { "fuel_type_id": 11, "fuel_type": "Alternative jet fuel", diff --git a/frontend/src/assets/locales/en/allocationAgreement.json b/frontend/src/assets/locales/en/allocationAgreement.json index cb01bb29a..1da1af078 100644 --- a/frontend/src/assets/locales/en/allocationAgreement.json +++ b/frontend/src/assets/locales/en/allocationAgreement.json @@ -4,7 +4,7 @@ "addAllocationAgreementRowsTitle": "Allocation agreements (e.g., allocating responsibility for fuel)", "allocationAgreementSubtitle": "Enter allocation agreement details below", "allocationAgreementColLabels": { - "transaction": "Transaction", + "transaction": "Responsibility", "transactionPartner": "Legal name of transaction partner", "postalAddress": "Address for service", "transactionPartnerEmail": "Email", diff --git a/frontend/src/assets/locales/en/transfer.json b/frontend/src/assets/locales/en/transfer.json index f0e5ffc25..14ba125bd 100644 --- a/frontend/src/assets/locales/en/transfer.json +++ b/frontend/src/assets/locales/en/transfer.json @@ -51,7 +51,7 @@ "loadingText": "Loading transfer...", "processingText": "Processing transfer...", "detailsLabel": "Transfer Details (required)", - "fairMarketText": "The fair market value of any consideration, in CAD", + "fairMarketText": "The fair market value of any consideration, in $CAD", "totalValueText": " per compliance unit for a total value of ", "saLabel": "Signing Authority Declaration", "saConfirmation": "I confirm that records evidencing each matter reported under section 18 of the Low Carbon Fuel (General) Regulation are available on request.", From d13584cd882e7d995eb2358c346acc8c49284d12 Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Wed, 4 Dec 2024 17:50:22 -0800 Subject: [PATCH 37/38] fix: front end test --- .../__tests__/AssessmentCard.test.jsx | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx b/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx index 0c458eb19..8cfda28ad 100644 --- a/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx +++ b/frontend/src/views/ComplianceReports/components/__tests__/AssessmentCard.test.jsx @@ -147,45 +147,6 @@ describe('AssessmentCard', () => { ).toBeInTheDocument() }) }) - it('filters out DRAFT status from history except when hasSupplemental is true', async () => { - const historyWithDraft = [ - ...mockHistory, - { - status: { status: COMPLIANCE_REPORT_STATUSES.DRAFT }, - createDate: '2024-08-01', - userProfile: { firstName: 'Alice', lastName: 'Wong' } - } - ] - - const mockChain = [ - { - history: historyWithDraft, - version: 0, - compliancePeriod: { - description: '2024' - }, - currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED }, - hasSupplemental: true - } - ] - - render( - , - { wrapper } - ) - await waitFor(() => { - expect(screen.getByText(/Alice Wong/)).toBeInTheDocument() - }) - }) it('filters out DRAFT status from history', async () => { const historyWithDraft = [ From 1523551568d8758fe0ed62b307571f8924aeb60a Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Wed, 4 Dec 2024 23:35:04 -0800 Subject: [PATCH 38/38] fix: synchronous boto3 s3 dependency --- backend/lcfs/services/s3/client.py | 41 +-- backend/lcfs/services/s3/dependency.py | 40 ++- backend/lcfs/services/s3/lifetime.py | 31 --- backend/lcfs/web/lifetime.py | 5 - backend/poetry.lock | 370 +------------------------ backend/pyproject.toml | 1 - 6 files changed, 42 insertions(+), 446 deletions(-) delete mode 100644 backend/lcfs/services/s3/lifetime.py diff --git a/backend/lcfs/services/s3/client.py b/backend/lcfs/services/s3/client.py index 309147971..260f8b0ea 100644 --- a/backend/lcfs/services/s3/client.py +++ b/backend/lcfs/services/s3/client.py @@ -3,10 +3,8 @@ from fastapi import Depends from pydantic.v1 import ValidationError from sqlalchemy import select -from sqlalchemy.exc import InvalidRequestError from sqlalchemy.ext.asyncio import AsyncSession from lcfs.services.s3.dependency import get_s3_client - from lcfs.db.dependencies import get_async_db_session from lcfs.db.models.compliance import ComplianceReport from lcfs.db.models.compliance.ComplianceReport import ( @@ -18,7 +16,6 @@ from lcfs.web.core.decorators import repo_handler BUCKET_NAME = settings.s3_bucket - MAX_FILE_SIZE_MB = 50 MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes @@ -43,21 +40,20 @@ async def upload_file(self, file, parent_id: str, parent_type="compliance_report file_size = os.fstat(file.file.fileno()).st_size if file_size > MAX_FILE_SIZE_BYTES: - raise InvalidRequestError( - detail=f"File size exceeds the maximum limit of {MAX_FILE_SIZE_MB} MB.", + raise ValidationError( + f"File size exceeds the maximum limit of {MAX_FILE_SIZE_MB} MB." ) if settings.clamav_enabled: self.clamav_service.scan_file(file) # Upload file to S3 - async with self.s3_client as client: - await client.upload_fileobj( - Fileobj=file.file, - Bucket=BUCKET_NAME, - Key=file_key, - ExtraArgs={"ContentType": file.content_type}, - ) + self.s3_client.upload_fileobj( + Fileobj=file.file, + Bucket=BUCKET_NAME, + Key=file_key, + ExtraArgs={"ContentType": file.content_type}, + ) document = Document( file_key=file_key, @@ -88,7 +84,6 @@ async def upload_file(self, file, parent_id: str, parent_type="compliance_report return document - # Generate a pre-signed URL for downloading a file from S3 @repo_handler async def generate_presigned_url(self, document_id: int): document = await self.db.get_one(Document, document_id) @@ -96,15 +91,13 @@ async def generate_presigned_url(self, document_id: int): if not document: raise Exception("Document not found") - async with self.s3_client as client: - presigned_url = await client.generate_presigned_url( - "get_object", - Params={"Bucket": BUCKET_NAME, "Key": document.file_key}, - ExpiresIn=60, # URL expiration in seconds - ) + presigned_url = self.s3_client.generate_presigned_url( + "get_object", + Params={"Bucket": BUCKET_NAME, "Key": document.file_key}, + ExpiresIn=60, # URL expiration in seconds + ) return presigned_url - # Delete a file from S3 and remove the entry from the database @repo_handler async def delete_file(self, document_id: int): document = await self.db.get_one(Document, document_id) @@ -113,8 +106,7 @@ async def delete_file(self, document_id: int): raise Exception("Document not found") # Delete the file from S3 - async with self.s3_client as client: - await client.delete_object(Bucket=BUCKET_NAME, Key=document.file_key) + self.s3_client.delete_object(Bucket=BUCKET_NAME, Key=document.file_key) # Delete the entry from the database await self.db.delete(document) @@ -145,8 +137,5 @@ async def get_object(self, document_id: int): if not document: raise Exception("Document not found") - async with self.s3_client as client: - response = await client.get_object( - Bucket=BUCKET_NAME, Key=document.file_key - ) + response = self.s3_client.get_object(Bucket=BUCKET_NAME, Key=document.file_key) return response, document diff --git a/backend/lcfs/services/s3/dependency.py b/backend/lcfs/services/s3/dependency.py index 4a30872f2..fc38ed388 100644 --- a/backend/lcfs/services/s3/dependency.py +++ b/backend/lcfs/services/s3/dependency.py @@ -1,20 +1,32 @@ -from starlette.requests import Request -from aioboto3 import Session +import boto3 +from typing import Generator +from lcfs.settings import settings -# S3 Client Dependency -async def get_s3_client( - request: Request, -) -> Session.client: +def get_s3_client() -> Generator: """ - Returns the S3 client from the application state. + Dependency function to provide a synchronous S3 client using boto3. - Usage: - >>> async def handler(s3_client = Depends(get_s3_client)): - >>> async with s3_client as client: - >>> await client.upload_fileobj('file.txt', 'my-bucket', 'file.txt') + This function creates a new S3 client session for each request that requires it. + The client is properly configured with the necessary AWS credentials and + endpoint settings. - :param request: Current request object. - :returns: S3 client. + Usage: + >>> def some_endpoint(s3_client = Depends(get_s3_client)): + >>> # Use the s3_client here """ - return request.app.state.s3_client + # Initialize the S3 client with the required configurations + client = boto3.client( + "s3", + aws_access_key_id=settings.s3_access_key, # Your AWS access key + aws_secret_access_key=settings.s3_secret_key, # Your AWS secret key + endpoint_url=settings.s3_endpoint, # Custom S3 endpoint (if any) + region_name="us-east-1", # AWS region + ) + + try: + # Yield the S3 client to be used within the request scope + yield client + finally: + # boto3 clients do not require explicit closing, but this ensures cleanup if needed + pass diff --git a/backend/lcfs/services/s3/lifetime.py b/backend/lcfs/services/s3/lifetime.py deleted file mode 100644 index ce853bf3c..000000000 --- a/backend/lcfs/services/s3/lifetime.py +++ /dev/null @@ -1,31 +0,0 @@ -from fastapi import FastAPI -from aioboto3 import Session -from lcfs.settings import settings - - -async def init_s3(app: FastAPI) -> None: - """ - Initialize the S3 client and store it in the app state. - - :param app: FastAPI application. - """ - session = Session() - app.state.s3_client = session.client( - "s3", - aws_access_key_id=settings.s3_access_key, - aws_secret_access_key=settings.s3_secret_key, - endpoint_url=settings.s3_endpoint, - region_name="us-east-1", - ) - print("Async S3 client initialized.") - - -async def shutdown_s3(app: FastAPI) -> None: - """ - Cleanup the S3 client from the app state. - - :param app: FastAPI application. - """ - if hasattr(app.state, "s3_client") and app.state.s3_client: - await app.state.s3_client.close() - print("Async S3 client shutdown.") diff --git a/backend/lcfs/web/lifetime.py b/backend/lcfs/web/lifetime.py index 72d859b36..5de67c16c 100644 --- a/backend/lcfs/web/lifetime.py +++ b/backend/lcfs/web/lifetime.py @@ -9,7 +9,6 @@ from lcfs.services.rabbitmq.consumers import start_consumers, stop_consumers from lcfs.services.redis.lifetime import init_redis, shutdown_redis -from lcfs.services.s3.lifetime import init_s3, shutdown_s3 from lcfs.services.tfrs.redis_balance import init_org_balance_cache from lcfs.settings import settings @@ -62,9 +61,6 @@ async def _startup() -> None: # noqa: WPS430 await init_org_balance_cache(app) - # Initialize the S3 client - await init_s3(app) - # Setup RabbitMQ Listeners await start_consumers() @@ -86,7 +82,6 @@ async def _shutdown() -> None: # noqa: WPS430 await app.state.db_engine.dispose() await shutdown_redis(app) - await shutdown_s3(app) await stop_consumers() return _shutdown diff --git a/backend/poetry.lock b/backend/poetry.lock index f63fce6e4..dba1d2518 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -15,185 +15,6 @@ files = [ aiormq = ">=6.8.0,<6.9.0" yarl = "*" -[[package]] -name = "aioboto3" -version = "13.2.0" -description = "Async boto3 wrapper" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "aioboto3-13.2.0-py3-none-any.whl", hash = "sha256:fd894b8d319934dfd75285b58da35560670e57182d0148c54a3d4ee5da730c78"}, - {file = "aioboto3-13.2.0.tar.gz", hash = "sha256:92c3232e0bf7dcb5d921cd1eb8c5e0b856c3985f7c1cd32ab3cd51adc5c9b5da"}, -] - -[package.dependencies] -aiobotocore = {version = "2.15.2", extras = ["boto3"]} -aiofiles = ">=23.2.1" - -[package.extras] -chalice = ["chalice (>=1.24.0)"] -s3cse = ["cryptography (>=2.3.1)"] - -[[package]] -name = "aiobotocore" -version = "2.15.2" -description = "Async client for aws services using botocore and aiohttp" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiobotocore-2.15.2-py3-none-any.whl", hash = "sha256:d4d3128b4b558e2b4c369bfa963b022d7e87303adb82eec623cec8aa77ae578a"}, - {file = "aiobotocore-2.15.2.tar.gz", hash = "sha256:9ac1cfcaccccc80602968174aa032bf978abe36bd4e55e6781d6500909af1375"}, -] - -[package.dependencies] -aiohttp = ">=3.9.2,<4.0.0" -aioitertools = ">=0.5.1,<1.0.0" -boto3 = {version = ">=1.35.16,<1.35.37", optional = true, markers = "extra == \"boto3\""} -botocore = ">=1.35.16,<1.35.37" -wrapt = ">=1.10.10,<2.0.0" - -[package.extras] -awscli = ["awscli (>=1.34.16,<1.35.3)"] -boto3 = ["boto3 (>=1.35.16,<1.35.37)"] - -[[package]] -name = "aiofiles" -version = "24.1.0" -description = "File support for asyncio." -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, - {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.4.4" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, - {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, -] - -[[package]] -name = "aiohttp" -version = "3.11.9" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0411777249f25d11bd2964a230b3ffafcbed6cd65d0f2b132bc2b8f5b8c347c7"}, - {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:499368eb904566fbdf1a3836a1532000ef1308f34a1bcbf36e6351904cced771"}, - {file = "aiohttp-3.11.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b5a5009b0159a8f707879dc102b139466d8ec6db05103ec1520394fdd8ea02c"}, - {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176f8bb8931da0613bb0ed16326d01330066bb1e172dd97e1e02b1c27383277b"}, - {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6435a66957cdba1a0b16f368bde03ce9c79c57306b39510da6ae5312a1a5b2c1"}, - {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:202f40fb686e5f93908eee0c75d1e6fbe50a43e9bd4909bf3bf4a56b560ca180"}, - {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"}, - {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6beeac698671baa558e82fa160be9761cf0eb25861943f4689ecf9000f8ebd0"}, - {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:96726839a42429318017e67a42cca75d4f0d5248a809b3cc2e125445edd7d50d"}, - {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3f5461c77649358610fb9694e790956b4238ac5d9e697a17f63619c096469afe"}, - {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4313f3bc901255b22f01663eeeae167468264fdae0d32c25fc631d5d6e15b502"}, - {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6e274661c74195708fc4380a4ef64298926c5a50bb10fbae3d01627d7a075b7"}, - {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db2914de2559809fdbcf3e48f41b17a493b58cb7988d3e211f6b63126c55fe82"}, - {file = "aiohttp-3.11.9-cp310-cp310-win32.whl", hash = "sha256:27935716f8d62c1c73010428db310fd10136002cfc6d52b0ba7bdfa752d26066"}, - {file = "aiohttp-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:afbe85b50ade42ddff5669947afde9e8a610e64d2c80be046d67ec4368e555fa"}, - {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:afcda759a69c6a8be3aae764ec6733155aa4a5ad9aad4f398b52ba4037942fe3"}, - {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5bba6b83fde4ca233cfda04cbd4685ab88696b0c8eaf76f7148969eab5e248a"}, - {file = "aiohttp-3.11.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:442356e8924fe1a121f8c87866b0ecdc785757fd28924b17c20493961b3d6697"}, - {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f737fef6e117856400afee4f17774cdea392b28ecf058833f5eca368a18cf1bf"}, - {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea142255d4901b03f89cb6a94411ecec117786a76fc9ab043af8f51dd50b5313"}, - {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1e9e447856e9b7b3d38e1316ae9a8c92e7536ef48373de758ea055edfd5db5"}, - {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f6173302f8a329ca5d1ee592af9e628d3ade87816e9958dcf7cdae2841def7"}, - {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c6147c6306f537cff59409609508a1d2eff81199f0302dd456bb9e7ea50c39"}, - {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9d036a9a41fc78e8a3f10a86c2fc1098fca8fab8715ba9eb999ce4788d35df0"}, - {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2ac9fd83096df36728da8e2f4488ac3b5602238f602706606f3702f07a13a409"}, - {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3108f0ad5c6b6d78eec5273219a5bbd884b4aacec17883ceefaac988850ce6e"}, - {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:96bbec47beb131bbf4bae05d8ef99ad9e5738f12717cfbbf16648b78b0232e87"}, - {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc726c3fa8f606d07bd2b500e5dc4c0fd664c59be7788a16b9e34352c50b6b6b"}, - {file = "aiohttp-3.11.9-cp311-cp311-win32.whl", hash = "sha256:5720ebbc7a1b46c33a42d489d25d36c64c419f52159485e55589fbec648ea49a"}, - {file = "aiohttp-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:17af09d963fa1acd7e4c280e9354aeafd9e3d47eaa4a6bfbd2171ad7da49f0c5"}, - {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1f2d7fd583fc79c240094b3e7237d88493814d4b300d013a42726c35a734bc9"}, - {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4b8a1b6c7a68c73191f2ebd3bf66f7ce02f9c374e309bdb68ba886bbbf1b938"}, - {file = "aiohttp-3.11.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd3f711f4c99da0091ced41dccdc1bcf8be0281dc314d6d9c6b6cf5df66f37a9"}, - {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cb1a1326a0264480a789e6100dc3e07122eb8cd1ad6b784a3d47d13ed1d89c"}, - {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a7ddf981a0b953ade1c2379052d47ccda2f58ab678fca0671c7c7ca2f67aac2"}, - {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ffa45cc55b18d4ac1396d1ddb029f139b1d3480f1594130e62bceadf2e1a838"}, - {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cca505829cdab58c2495ff418c96092d225a1bbd486f79017f6de915580d3c44"}, - {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44d323aa80a867cb6db6bebb4bbec677c6478e38128847f2c6b0f70eae984d72"}, - {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2fab23003c4bb2249729a7290a76c1dda38c438300fdf97d4e42bf78b19c810"}, - {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:be0c7c98e38a1e3ad7a6ff64af8b6d6db34bf5a41b1478e24c3c74d9e7f8ed42"}, - {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cc5e0d069c56645446c45a4b5010d4b33ac6c5ebfd369a791b5f097e46a3c08"}, - {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9bcf97b971289be69638d8b1b616f7e557e1342debc7fc86cf89d3f08960e411"}, - {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c7333e7239415076d1418dbfb7fa4df48f3a5b00f8fdf854fca549080455bc14"}, - {file = "aiohttp-3.11.9-cp312-cp312-win32.whl", hash = "sha256:9384b07cfd3045b37b05ed002d1c255db02fb96506ad65f0f9b776b762a7572e"}, - {file = "aiohttp-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:f5252ba8b43906f206048fa569debf2cd0da0316e8d5b4d25abe53307f573941"}, - {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:282e0a7ddd36ebc411f156aeaa0491e8fe7f030e2a95da532cf0c84b0b70bc66"}, - {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd3e6b0c7d4954cca59d241970011f8d3327633d555051c430bd09ff49dc494"}, - {file = "aiohttp-3.11.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30f9f89ae625d412043f12ca3771b2ccec227cc93b93bb1f994db6e1af40a7d3"}, - {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3b5b2c012d70c63d9d13c57ed1603709a4d9d7d473e4a9dfece0e4ea3d5f51"}, - {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ef1550bb5f55f71b97a6a395286db07f7f2c01c8890e613556df9a51da91e8d"}, - {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317251b9c9a2f1a9ff9cd093775b34c6861d1d7df9439ce3d32a88c275c995cd"}, - {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cbe97839b009826a61b143d3ca4964c8590d7aed33d6118125e5b71691ca46"}, - {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:618b18c3a2360ac940a5503da14fa4f880c5b9bc315ec20a830357bcc62e6bae"}, - {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0cf4d814689e58f57ecd5d8c523e6538417ca2e72ff52c007c64065cef50fb2"}, - {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:15c4e489942d987d5dac0ba39e5772dcbed4cc9ae3710d1025d5ba95e4a5349c"}, - {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8df0ff5a911c6d21957a9182402aad7bf060eaeffd77c9ea1c16aecab5adbf"}, - {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ed95d66745f53e129e935ad726167d3a6cb18c5d33df3165974d54742c373868"}, - {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:647ec5bee7e4ec9f1034ab48173b5fa970d9a991e565549b965e93331f1328fe"}, - {file = "aiohttp-3.11.9-cp313-cp313-win32.whl", hash = "sha256:ef2c9499b7bd1e24e473dc1a85de55d72fd084eea3d8bdeec7ee0720decb54fa"}, - {file = "aiohttp-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:84de955314aa5e8d469b00b14d6d714b008087a0222b0f743e7ffac34ef56aff"}, - {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e738aabff3586091221044b7a584865ddc4d6120346d12e28e788307cd731043"}, - {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28f29bce89c3b401a53d6fd4bee401ee943083bf2bdc12ef297c1d63155070b0"}, - {file = "aiohttp-3.11.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31de2f10f63f96cc19e04bd2df9549559beadd0b2ee2da24a17e7ed877ca8c60"}, - {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f31cebd8c27a36af6c7346055ac564946e562080ee1a838da724585c67474f"}, - {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bcb7f6976dc0b6b56efde13294862adf68dd48854111b422a336fa729a82ea6"}, - {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8b13b9950d8b2f8f58b6e5842c4b842b5887e2c32e3f4644d6642f1659a530"}, - {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c23e62f3545c2216100603614f9e019e41b9403c47dd85b8e7e5015bf1bde0"}, - {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec656680fc53a13f849c71afd0c84a55c536206d524cbc831cde80abbe80489e"}, - {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:36df00e0541f264ce42d62280281541a47474dfda500bc5b7f24f70a7f87be7a"}, - {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8dcfd14c712aa9dd18049280bfb2f95700ff6a8bde645e09f17c3ed3f05a0130"}, - {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14624d96f0d69cf451deed3173079a68c322279be6030208b045ab77e1e8d550"}, - {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4b01d9cfcb616eeb6d40f02e66bebfe7b06d9f2ef81641fdd50b8dd981166e0b"}, - {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:928f92f80e2e8d6567b87d3316c1fd9860ccfe36e87a9a7f5237d4cda8baa1ba"}, - {file = "aiohttp-3.11.9-cp39-cp39-win32.whl", hash = "sha256:c8a02f74ae419e3955af60f570d83187423e42e672a6433c5e292f1d23619269"}, - {file = "aiohttp-3.11.9-cp39-cp39-win_amd64.whl", hash = "sha256:0a97d657f6cf8782a830bb476c13f7d777cfcab8428ac49dde15c22babceb361"}, - {file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.3.0" -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] - -[[package]] -name = "aioitertools" -version = "0.12.0" -description = "itertools and builtins for AsyncIO and mixed iterables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796"}, - {file = "aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b"}, -] - -[package.dependencies] -typing_extensions = {version = ">=4.0", markers = "python_version < \"3.10\""} - -[package.extras] -dev = ["attribution (==1.8.0)", "black (==24.8.0)", "build (>=1.2)", "coverage (==7.6.1)", "flake8 (==7.1.1)", "flit (==3.9.0)", "mypy (==1.11.2)", "ufmt (==2.7.1)", "usort (==1.0.8.post1)"] -docs = ["sphinx (==8.0.2)", "sphinx-mdinclude (==0.6.2)"] - [[package]] name = "aiormq" version = "6.8.1" @@ -209,20 +30,6 @@ files = [ pamqp = "3.3.0" yarl = "*" -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - [[package]] name = "alembic" version = "1.14.0" @@ -1325,107 +1132,6 @@ files = [ [package.dependencies] flake8 = "*" -[[package]] -name = "frozenlist" -version = "1.5.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, - {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, - {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, - {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, - {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, - {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, - {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, - {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, - {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, - {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, - {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, - {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, - {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, - {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, - {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, - {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, - {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, -] - [[package]] name = "greenlet" version = "3.1.1" @@ -3952,80 +3658,6 @@ pep8-naming = ">=0.13,<0.14" pygments = ">=2.4,<3.0" typing_extensions = ">=4.0,<5.0" -[[package]] -name = "wrapt" -version = "1.17.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -files = [ - {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, - {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, - {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, - {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, - {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, - {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, - {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, - {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, - {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, - {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, - {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, - {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, - {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, - {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, - {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, - {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, - {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, - {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, - {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, - {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, - {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, - {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, - {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, -] - [[package]] name = "xlrd" version = "2.0.1" @@ -4152,4 +3784,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "4fd345bed13bd10e81d85a185b72c1ff3cbbb006734b6f812ea144976a2bf0ec" +content-hash = "6ac5bbf72db0f71b3519fd274af81ebfa064cad93bdf34c801568939d6e4214f" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 45f128340..120fe80d5 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -46,7 +46,6 @@ python-multipart = "^0.0.16" aio-pika = "^9.4.3" jinja2 = "^3.1.4" requests = "^2.32.3" -aioboto3 = "^13.2.0" [tool.poetry.dev-dependencies] pytest = "^8.3.3"