routers.sellers
Endpoints for sellers.
--- config: mirrorActors: false --- sequenceDiagram title Seller Registration actor user box ./routers participant sellers.py@{ "type" : "boundary" } end box ./internal/database participant dd as database.py end box ./internal/auth participant creation.py participant security.py end box ./internal/queries participant user.py participant sq as seller.py end participant database@{ "type" : "database" } user->>sellers.py: register seller activate sellers.py dd->>sellers.py: yield connection activate dd sellers.py->>creation.py: await create_seller() activate creation.py creation.py->>creation.py: create_user() creation.py->>security.py: hash_password() activate security.py security.py-->>creation.py: password hash deactivate security.py creation.py->>user.py: Queries.create_user() activate user.py user.py->>database: insert user activate database database-->>user.py: created user deactivate database user.py-->>creation.py: created user deactivate user.py creation.py-->>creation.py: created user creation.py->>sq: Queries.create_seller() activate sq sq->>database: insert seller activate database database-->>sq: created seller deactivate database sq-->>creation.py: created seller deactivate sq creation.py-->>sellers.py: created seller deactivate creation.py sellers.py-->>user: 201 OK sellers.py-->>dd: return connection deactivate dd deactivate sellers.py
1"""Endpoints for sellers. 2 3```mermaid 4--- 5config: 6 mirrorActors: false 7--- 8sequenceDiagram 9 title Seller Registration 10 actor user 11 box ./routers 12 participant sellers.py@{ "type" : "boundary" } 13 end 14 box ./internal/database 15 participant dd as database.py 16 end 17 box ./internal/auth 18 participant creation.py 19 participant security.py 20 end 21 box ./internal/queries 22 participant user.py 23 participant sq as seller.py 24 end 25 participant database@{ "type" : "database" } 26 27 user->>sellers.py: register seller 28 activate sellers.py 29 dd->>sellers.py: yield connection 30 activate dd 31 sellers.py->>creation.py: await create_seller() 32 activate creation.py 33 creation.py->>creation.py: create_user() 34 creation.py->>security.py: hash_password() 35 activate security.py 36 security.py-->>creation.py: password hash 37 deactivate security.py 38 creation.py->>user.py: Queries.create_user() 39 activate user.py 40 user.py->>database: insert user 41 activate database 42 database-->>user.py: created user 43 deactivate database 44 user.py-->>creation.py: created user 45 deactivate user.py 46 creation.py-->>creation.py: created user 47 creation.py->>sq: Queries.create_seller() 48 activate sq 49 sq->>database: insert seller 50 activate database 51 database-->>sq: created seller 52 deactivate database 53 sq-->>creation.py: created seller 54 deactivate sq 55 creation.py-->>sellers.py: created seller 56 deactivate creation.py 57 sellers.py-->>user: 201 OK 58 sellers.py-->>dd: return connection 59 deactivate dd 60 deactivate sellers.py 61``` 62""" 63 64from datetime import datetime 65from decimal import Decimal 66from typing import Annotated 67 68from fastapi import APIRouter, Depends, HTTPException, Response, UploadFile, status 69from internal.analytics.co2_estimator import estimate_carbon_doixide_saved 70from internal.analytics.processing import AnalyticsProcesser 71from internal.auth.creation import CreateSellerForm, create_seller 72from internal.auth.middleware import SellerAuthDep 73from internal.badges.engine import BadgeEngine 74from internal.block.management import block_management 75from internal.database.dependency import database_dependency 76from internal.queries.allergens import ( 77 AddBundlesAllergenParams, 78 DeleteBundleAllergenParams, 79) 80from internal.queries.allergens import AsyncQuerier as AllergenQuerier 81from internal.queries.analytics import AsyncQuerier as AnalyticsQuerier 82from internal.queries.analytics import GetGraphParams 83from internal.queries.bundle import AsyncQuerier as BundleQuerier 84from internal.queries.bundle import ( 85 CreateBundleParams, 86 GetSellersBundleParams, 87 UpdateBundleParams, 88) 89from internal.queries.category import ( 90 AddBundlesCategoryParams, 91 DeleteBundleCategoryParams, 92) 93from internal.queries.category import AsyncQuerier as CategoryQuerier 94from internal.queries.forecast import AsyncQuerier as ForecastQuerier 95from internal.queries.models import ( 96 AnalyticsGraph, 97 AnalyticsGraphsType, 98 AnalyticsPoint, 99 AnalyticsSeries, 100 Bundle, 101 ForecastOutput, 102 Reservation, 103) 104from internal.queries.reservations import AsyncQuerier as ReservationsQuerier 105from internal.queries.reservations import GetReservationCollectionParams 106from internal.queries.seller import AsyncQuerier as SellerQuerier 107from internal.queries.seller import GetSellerRow, GetSellersRow, UpdateSellerParams 108from pydantic import BaseModel, Field 109 110router = APIRouter(prefix="/sellers", tags=["sellers"]) 111 112 113@router.get( 114 "", 115 status_code=status.HTTP_200_OK, 116 summary="Get all sellers", 117 description="Retrieves a list of all registered sellers.", 118) 119async def get_sellers(conn: database_dependency) -> list[GetSellersRow]: 120 """Get all sellers. 121 122 Args: 123 conn: database connection 124 125 Returns: 126 list of all sellers 127 """ 128 return [seller async for seller in SellerQuerier(conn).get_sellers()] 129 130 131@router.get( 132 "/me", 133 status_code=status.HTTP_200_OK, 134 summary="Get authenticated seller", 135 description="Retrieves the profile of the authenticated seller.", 136) 137async def get_seller_me( 138 conn: database_dependency, seller: SellerAuthDep 139) -> GetSellerRow: 140 """Get authenticated seller profile. 141 142 Args: 143 conn: database connection 144 seller: sellers session 145 146 Returns: 147 seller profile 148 149 Raises: 150 HTTPException: if seller not found 151 """ 152 seller_profile = await SellerQuerier(conn).get_seller(user_id=seller.user_id) 153 if not seller_profile: 154 raise HTTPException( 155 status_code=status.HTTP_404_NOT_FOUND, detail="Seller profile not found" 156 ) 157 return seller_profile 158 159 160@router.get( 161 "/{seller_id}", 162 status_code=status.HTTP_200_OK, 163 summary="Get seller by ID", 164 description="Retrieves the profile of a seller by their unique ID.", 165) 166async def get_seller_by_id(seller_id: int, conn: database_dependency) -> GetSellerRow: 167 """Get seller profile by ID. 168 169 Args: 170 seller_id: unique identifier of the seller 171 conn: database connection 172 173 Returns: 174 seller profile 175 176 Raises: 177 HTTPException: if seller not found 178 """ 179 seller_profile = await SellerQuerier(conn).get_seller(user_id=seller_id) 180 if not seller_profile: 181 raise HTTPException( 182 status_code=status.HTTP_404_NOT_FOUND, detail="Seller not found" 183 ) 184 return seller_profile 185 186 187@router.post( 188 "", 189 status_code=status.HTTP_201_CREATED, 190 summary="Register seller", 191 description="Creates a new seller and their corresponding user entity.", 192) 193async def register_seller(form: CreateSellerForm, conn: database_dependency) -> None: 194 """Creates seller and coressponding user. 195 196 Args: 197 form: signup form from user 198 conn: database connection 199 """ 200 _ = await create_seller(form, conn) 201 202 203class UpdateSellerForm(BaseModel): 204 """Form for updating seller profile.""" 205 206 address_line1: str 207 address_line2: str | None = None 208 city: str 209 post_code: str 210 region: str | None = None 211 country: str 212 latitude: float | None = None 213 longitude: float | None = None 214 215 216@router.patch( 217 "/me", 218 status_code=status.HTTP_200_OK, 219 summary="Update seller profile", 220 description="Updates the profile information for the authenticated seller.", 221) 222async def update_seller( 223 form: UpdateSellerForm, conn: database_dependency, seller: SellerAuthDep 224) -> None: 225 """Update seller profile. 226 227 Args: 228 form: seller update form 229 conn: database connection 230 seller: seller session 231 232 Raises: 233 HTTPException: if failed to update seller 234 """ 235 seller_querier = SellerQuerier(conn) 236 seller_record = await seller_querier.get_seller(user_id=seller.user_id) 237 if seller_record is None: 238 raise HTTPException( 239 status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to get seller" 240 ) 241 updated_seller = await seller_querier.update_seller( 242 UpdateSellerParams( 243 user_id=seller.user_id, 244 seller_name=seller_record.seller_name, 245 address_line1=form.address_line1, 246 address_line2=form.address_line2, 247 city=form.city, 248 post_code=form.post_code, 249 region=form.region, 250 country=form.country, 251 latitude=form.latitude, 252 longitude=form.longitude, 253 ) 254 ) 255 if not updated_seller: 256 raise HTTPException( 257 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 258 detail="Failed to update seller", 259 ) 260 261 262class BundleForm(BaseModel): 263 """User form for bundles.""" 264 265 bundle_name: str 266 description: str 267 total_qty: int 268 price: Decimal = Field(decimal_places=2, gt=0) 269 discount_percentage: int = Field(lt=100, gt=0) 270 weight: int 271 categories: list[int] = Field(min_length=1) 272 allergens: list[int] 273 window_start: datetime 274 window_end: datetime 275 276 277@router.post( 278 "/me/bundles", 279 tags=["bundles"], 280 status_code=status.HTTP_201_CREATED, 281 summary="Create bundle", 282 description="Creates a new bundle for the authenticated seller.", 283) 284async def create_bundle( 285 form: BundleForm, conn: database_dependency, seller: SellerAuthDep 286) -> Bundle: 287 """Create bundle. 288 289 Args: 290 form: bundle info form 291 conn: database connection 292 seller: sellers session 293 294 Returns: 295 created bundle 296 297 Raises: 298 HTTPException: if failed to create bundle 299 """ 300 category_querier = CategoryQuerier(conn) 301 allergen_querier = AllergenQuerier(conn) 302 coefficients: list[float] = [] 303 for category in form.categories: 304 if ( 305 category_record := await category_querier.get_category(category_id=category) 306 ) is None: 307 raise HTTPException( 308 status.HTTP_500_INTERNAL_SERVER_ERROR, 309 "Failed to get category coefficient", 310 ) 311 coefficients.append(category_record.category_coefficient) 312 carbon_dioxide = estimate_carbon_doixide_saved(coefficients, weight_g=form.weight) 313 bundle = await BundleQuerier(conn).create_bundle( 314 CreateBundleParams( 315 seller_id=seller.user_id, 316 bundle_name=form.bundle_name, 317 description=form.description, 318 total_qty=form.total_qty, 319 price=form.price, 320 carbon_dioxide=carbon_dioxide, 321 discount_percentage=form.discount_percentage, 322 window_start=form.window_start, 323 window_end=form.window_end, 324 ) 325 ) 326 if not bundle: 327 raise HTTPException( 328 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 329 detail="Failed to create bundle", 330 ) 331 for category in form.categories: 332 bundle_category = await category_querier.add_bundles_category( 333 AddBundlesCategoryParams(bundle_id=bundle.bundle_id, category_id=category) 334 ) 335 if bundle_category is None: 336 raise HTTPException( 337 status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to add bundle category" 338 ) 339 for allergen in form.allergens: 340 bundle_allergen = await allergen_querier.add_bundles_allergen( 341 AddBundlesAllergenParams(bundle_id=bundle.bundle_id, allergen_id=allergen) 342 ) 343 if bundle_allergen is None: 344 raise HTTPException( 345 status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to add bundle allergen" 346 ) 347 return bundle 348 349 350async def update_bundle_categories( 351 bundle_id: int, category_querier: CategoryQuerier, form: BundleForm 352) -> None: 353 """Update bundle categories to a new once. 354 355 Args: 356 bundle_id: bundle id 357 category_querier: categories async querier 358 form: update bundle form 359 360 Raises: 361 HTTPException: if failed to update categories 362 """ 363 bundle_categories = [ 364 bundle_category 365 async for bundle_category in category_querier.get_bundle_categories( 366 bundle_id=bundle_id 367 ) 368 ] 369 for category in form.categories: 370 if category not in bundle_categories: 371 added_category = await category_querier.add_bundles_category( 372 AddBundlesCategoryParams(bundle_id=bundle_id, category_id=category) 373 ) 374 if added_category is None: 375 raise HTTPException( 376 status.HTTP_500_INTERNAL_SERVER_ERROR, 377 "Failed to add bundle category", 378 ) 379 for bundle_category in bundle_categories: 380 if bundle_category not in form.categories: 381 deleted_category = await category_querier.delete_bundle_category( 382 DeleteBundleCategoryParams( 383 bundle_id=bundle_id, category_id=bundle_category 384 ) 385 ) 386 if deleted_category is None: 387 raise HTTPException( 388 status.HTTP_500_INTERNAL_SERVER_ERROR, 389 "failed to delete bundle category", 390 ) 391 392 393async def update_bundle_allergens( 394 bundle_id: int, allergen_querier: AllergenQuerier, form: BundleForm 395) -> None: 396 """Update bandle allergens to a new once. 397 398 Args: 399 bundle_id: bundle id 400 allergen_querier: allergen async querier 401 form: bundle update form 402 403 Raises: 404 HTTPException: if failed to update allergens 405 """ 406 bundle_allergens = [ 407 bundle_allergen 408 async for bundle_allergen in allergen_querier.get_bundle_allergens( 409 bundle_id=bundle_id 410 ) 411 ] 412 for allergen in form.allergens: 413 if allergen not in bundle_allergens: 414 added_allergen = await allergen_querier.add_bundles_allergen( 415 AddBundlesAllergenParams(bundle_id=bundle_id, allergen_id=allergen) 416 ) 417 if added_allergen is None: 418 raise HTTPException( 419 status.HTTP_500_INTERNAL_SERVER_ERROR, 420 "Failed to add bundle allergen", 421 ) 422 for bundle_allergen in bundle_allergens: 423 if bundle_allergen not in form.allergens: 424 deleted_allergen = await allergen_querier.delete_bundle_allergen( 425 DeleteBundleAllergenParams( 426 bundle_id=bundle_id, allergen_id=bundle_allergen 427 ) 428 ) 429 if deleted_allergen is None: 430 raise HTTPException( 431 status.HTTP_500_INTERNAL_SERVER_ERROR, 432 "failed to delete bundle allergen", 433 ) 434 435 436@router.patch( 437 "/me/bundles/{bundle_id}", 438 tags=["bundles"], 439 status_code=status.HTTP_200_OK, 440 summary="Update bundle", 441 description="Updates an existing bundle for the authenticated seller.", 442) 443async def update_bundle( 444 bundle_id: int, form: BundleForm, conn: database_dependency, seller: SellerAuthDep 445) -> Bundle: 446 """Update bundle. 447 448 Args: 449 bundle_id: bundle id 450 form: updated bundle info form 451 conn: database connection 452 seller: sellers session 453 454 Returns: 455 updated bundle 456 457 Raises: 458 HTTPException: if failed to update bundle 459 """ 460 category_querier = CategoryQuerier(conn) 461 allergen_querier = AllergenQuerier(conn) 462 coefficients: list[float] = [] 463 for category in form.categories: 464 if ( 465 category_record := await category_querier.get_category(category_id=category) 466 ) is None: 467 raise HTTPException( 468 status.HTTP_500_INTERNAL_SERVER_ERROR, 469 "Failed to get category coefficient", 470 ) 471 coefficients.append(category_record.category_coefficient) 472 carbon_dioxide = estimate_carbon_doixide_saved(coefficients, weight_g=form.weight) 473 bundle = await BundleQuerier(conn).update_bundle( 474 UpdateBundleParams( 475 bundle_id=bundle_id, 476 seller_id=seller.user_id, 477 bundle_name=form.bundle_name, 478 description=form.description, 479 total_qty=form.total_qty, 480 price=form.price, 481 discount_percentage=form.discount_percentage, 482 window_start=form.window_start, 483 window_end=form.window_end, 484 carbon_dioxide=carbon_dioxide, 485 ) 486 ) 487 if not bundle: 488 raise HTTPException( 489 status_code=status.HTTP_404_NOT_FOUND, 490 detail="Bundle not found or not owned by seller", 491 ) 492 await update_bundle_categories(bundle_id, category_querier, form) 493 await update_bundle_allergens(bundle_id, allergen_querier, form) 494 return bundle 495 496 497@router.get( 498 "/me/bundles", 499 status_code=status.HTTP_200_OK, 500 summary="Get seller bundles", 501 description="Retrieves all bundles created by the authenticated seller.", 502) 503async def get_bundles(conn: database_dependency, seller: SellerAuthDep) -> list[Bundle]: 504 """Get sellers bundles. 505 506 Args: 507 conn: database connection 508 seller: sellers connection 509 510 Returns: 511 list of sellers bundles 512 513 Raises: 514 HTTPException: if failed to get bundles 515 """ 516 bundles = [ 517 item 518 async for item in BundleQuerier(conn).get_sellers_bundles( 519 seller_id=seller.user_id 520 ) 521 ] 522 if bundles is None: 523 raise HTTPException( 524 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 525 detail="Failed to get bundles", 526 ) 527 return list(bundles) 528 529 530@router.get( 531 "/me/bundles/{bundle_id}/reservations", 532 tags=["reservations"], 533 status_code=status.HTTP_200_OK, 534 summary="Get bundle reservations", 535 description=( 536 "Retrieves all reservations for a specific bundle owned " 537 "by the authenticated seller." 538 ), 539) 540async def get_reservations( 541 bundle_id: int, conn: database_dependency, seller: SellerAuthDep 542) -> list[Reservation]: 543 """Get reservations for sellers bundle. 544 545 Args: 546 bundle_id: bundle id 547 conn: database connection 548 seller: sellers session 549 550 Returns: 551 list of reservations for specific bundle 552 553 Raises: 554 HTTPException: if failed to get reservations 555 """ 556 bundle = await BundleQuerier(conn).get_sellers_bundle( 557 GetSellersBundleParams(bundle_id=bundle_id, seller_id=seller.user_id) 558 ) 559 if not bundle: 560 raise HTTPException( 561 status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found" 562 ) 563 reservations = [ 564 item 565 async for item in ReservationsQuerier(conn).get_bundle_reservations( 566 bundle_id=bundle.bundle_id 567 ) 568 ] 569 if reservations is None: 570 raise HTTPException( 571 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 572 detail="Failed to find bundle reservations", 573 ) 574 return list(reservations) 575 576 577@router.patch( 578 "/me/bundles/{bundle_id}/reservations/collect", 579 tags=["reservations"], 580 status_code=status.HTTP_200_OK, 581 summary="Collect reservation", 582 description="Confirms the collection of a reservation using a claim code.", 583) 584async def reservation_collection( 585 bundle_id: int, 586 claim_code: str, 587 conn: database_dependency, 588 seller: SellerAuthDep, 589 badge_engine: Annotated[BadgeEngine, Depends(BadgeEngine)], 590) -> Reservation: 591 """Confirm reservation collection. 592 593 Args: 594 bundle_id: bundle_id 595 claim_code: claim code 596 conn: database connection 597 seller: sellers session 598 badge_engine: badge acquiry engine 599 600 Returns: 601 confirmed claimed reservation 602 603 Raises: 604 HTTPException: if failed to collect reservation 605 """ 606 reservation_querier = ReservationsQuerier(conn) 607 reservation = await reservation_querier.get_reservation_collection( 608 GetReservationCollectionParams(bundle_id=bundle_id, claim_code=claim_code) 609 ) 610 if not reservation: 611 raise HTTPException( 612 status_code=status.HTTP_404_NOT_FOUND, detail="Reservation not found" 613 ) 614 bundle = await BundleQuerier(conn).get_bundle(bundle_id=reservation.bundle_id) 615 if not bundle: 616 raise HTTPException( 617 status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found" 618 ) 619 if bundle.seller_id != seller.user_id: 620 raise HTTPException( 621 status_code=status.HTTP_403_FORBIDDEN, 622 detail="Reservation does not belong to your bundle", 623 ) 624 claimed_reservation = await reservation_querier.collect_reservation( 625 reservation_id=reservation.reservation_id 626 ) 627 if not claimed_reservation: 628 raise HTTPException( 629 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 630 detail="Failed to update reservation status", 631 ) 632 await conn.commit() 633 badge_engine.run(claimed_reservation.consumer_id, bundle.window_start) 634 return claimed_reservation 635 636 637@router.post( 638 "/me/analytics", 639 tags=["analytics"], 640 summary="Refresh analytics", 641 description="Triggers a refresh of analytics graphs for the authenticated seller.", 642) 643async def refresh_analytics( 644 seller: SellerAuthDep, 645 analytics_processer: Annotated[AnalyticsProcesser, Depends(AnalyticsProcesser)], 646) -> None: 647 """Refresh analytics graphs for the authenticated seller. 648 649 Args: 650 seller: authenticated seller session 651 analytics_processer: analytics processing engine 652 """ 653 analytics_processer.run(seller.user_id) 654 655 656@router.patch( 657 path="/me/bundles/{bundle_id}/image", 658 status_code=status.HTTP_202_ACCEPTED, 659 summary="Change bundle image", 660 description="Updates the image for a bundle owned by the authenticated seller.", 661) 662async def change_bundle_image( 663 bundle_id: int, file: UploadFile, conn: database_dependency, seller: SellerAuthDep 664) -> None: 665 """Change bundle image. 666 667 Args: 668 bundle_id: bundle id 669 file: bundle image 670 conn: database connection 671 seller: seller session 672 673 Raises: 674 HTTPException: if failed to change image 675 """ 676 if ( 677 BundleQuerier(conn).get_sellers_bundle( 678 GetSellersBundleParams(seller_id=seller.user_id, bundle_id=bundle_id) 679 ) 680 is None 681 ): 682 raise HTTPException(status.HTTP_404_NOT_FOUND, "bundle not found") 683 await block_management.upload_bundle_image(bundle_id, file) 684 685 686@router.get( 687 path="/me/bundles/{bundle_id}/image", 688 status_code=status.HTTP_200_OK, 689 summary="Get bundle image", 690 description="Retrieves the image for a bundle owned by the authenticated seller.", 691) 692async def get_bundle_image( 693 bundle_id: int, conn: database_dependency, seller: SellerAuthDep 694) -> Response: 695 """Get bundle image. 696 697 Args: 698 bundle_id: bundle id 699 conn: database connection 700 seller: seller session 701 702 Returns: 703 bundle image 704 705 Raises: 706 HTTPException: if failed to get bundle image 707 """ 708 if ( 709 BundleQuerier(conn).get_sellers_bundle( 710 GetSellersBundleParams(seller_id=seller.user_id, bundle_id=bundle_id) 711 ) 712 is None 713 ): 714 raise HTTPException(status.HTTP_404_NOT_FOUND, "bundle not found") 715 return Response( 716 block_management.get_bundle_image(bundle_id), media_type="image/jpeg" 717 ) 718 719 720@router.get( 721 "/me/analytics", 722 status_code=status.HTTP_200_OK, 723 tags=["analytics"], 724 summary="Get analytics graph types", 725 description="Retrieves all available analytics graph types for the seller.", 726) 727async def get_analytics_graph_types( 728 conn: database_dependency, _: SellerAuthDep 729) -> list[AnalyticsGraphsType]: 730 """Get all graph types. 731 732 Args: 733 conn: database connection 734 735 Returns: 736 list of all graph types 737 """ 738 return [ 739 graph_type async for graph_type in AnalyticsQuerier(conn).get_graphs_types() 740 ] 741 742 743@router.get( 744 "/me/analytics/{graph_type_id}", 745 status_code=status.HTTP_200_OK, 746 tags=["analytics"], 747 summary="Get analytics graph", 748 description="Retrieves an analytics graph with series and points for the seller.", 749) 750async def get_analytics_graph( 751 graph_type_id: int, conn: database_dependency, seller: SellerAuthDep 752) -> tuple[AnalyticsGraph, list[tuple[AnalyticsSeries, list[AnalyticsPoint]]]]: 753 """Get analytics graph. 754 755 Args: 756 graph_type_id: graph type id 757 conn: database connection 758 seller: seller session 759 760 Returns: 761 graph, series and points 762 763 Raises: 764 HTTPException: if failed to get graph 765 """ 766 analytics_querier = AnalyticsQuerier(conn) 767 if (await analytics_querier.get_graph_type(graph_type_id=graph_type_id)) is None: 768 raise HTTPException(status.HTTP_404_NOT_FOUND, "failed to find graph type") 769 if ( 770 graph := await analytics_querier.get_graph( 771 GetGraphParams(seller_id=seller.user_id, graph_type=graph_type_id) 772 ) 773 ) is None: 774 raise HTTPException( 775 status.HTTP_500_INTERNAL_SERVER_ERROR, "failed to get graph" 776 ) 777 series = [ 778 series 779 async for series in analytics_querier.get_graph_series(graph_id=graph.graph_id) 780 ] 781 if len(series) == 0: 782 return (graph, []) 783 series_and_points: list[tuple[AnalyticsSeries, list[AnalyticsPoint]]] = [] 784 for single_series in series: 785 single_series_points: list[AnalyticsPoint] = [ 786 point 787 async for point in analytics_querier.get_graph_points( 788 series_id=single_series.series_id 789 ) 790 ] 791 series_and_points.append((single_series, single_series_points)) 792 return (graph, series_and_points) 793 794 795@router.get( 796 "/me/bundles/{bundle_id}/forecasting", 797 status_code=status.HTTP_200_OK, 798 tags=["forecasts"], 799 summary="Get bundle forecast", 800 description="Retrieves a forecast for a specific bundle.", 801) 802async def get_bundle_forecast( 803 bundle_id: int, conn: database_dependency, seller: SellerAuthDep 804) -> ForecastOutput: 805 """Get forecast for a specific bundle. 806 807 Args: 808 bundle_id: bundle id 809 conn: database connection 810 seller: seller session 811 812 Returns: 813 forecast for the bundle 814 815 Raises: 816 HTTPException: if forecast not found 817 """ 818 forecast = await ForecastQuerier(conn).get_forecast_output_by_bundle( 819 bundle_id=bundle_id 820 ) 821 if forecast is None: 822 raise HTTPException(status.HTTP_404_NOT_FOUND, "forecast not found") 823 if forecast.seller_id != seller.user_id: 824 raise HTTPException(status.HTTP_403_FORBIDDEN, "not your forecast") 825 return forecast
114@router.get( 115 "", 116 status_code=status.HTTP_200_OK, 117 summary="Get all sellers", 118 description="Retrieves a list of all registered sellers.", 119) 120async def get_sellers(conn: database_dependency) -> list[GetSellersRow]: 121 """Get all sellers. 122 123 Args: 124 conn: database connection 125 126 Returns: 127 list of all sellers 128 """ 129 return [seller async for seller in SellerQuerier(conn).get_sellers()]
Get all sellers.
Arguments:
- conn: database connection
Returns:
list of all sellers
132@router.get( 133 "/me", 134 status_code=status.HTTP_200_OK, 135 summary="Get authenticated seller", 136 description="Retrieves the profile of the authenticated seller.", 137) 138async def get_seller_me( 139 conn: database_dependency, seller: SellerAuthDep 140) -> GetSellerRow: 141 """Get authenticated seller profile. 142 143 Args: 144 conn: database connection 145 seller: sellers session 146 147 Returns: 148 seller profile 149 150 Raises: 151 HTTPException: if seller not found 152 """ 153 seller_profile = await SellerQuerier(conn).get_seller(user_id=seller.user_id) 154 if not seller_profile: 155 raise HTTPException( 156 status_code=status.HTTP_404_NOT_FOUND, detail="Seller profile not found" 157 ) 158 return seller_profile
Get authenticated seller profile.
Arguments:
- conn: database connection
- seller: sellers session
Returns:
seller profile
Raises:
- HTTPException: if seller not found
161@router.get( 162 "/{seller_id}", 163 status_code=status.HTTP_200_OK, 164 summary="Get seller by ID", 165 description="Retrieves the profile of a seller by their unique ID.", 166) 167async def get_seller_by_id(seller_id: int, conn: database_dependency) -> GetSellerRow: 168 """Get seller profile by ID. 169 170 Args: 171 seller_id: unique identifier of the seller 172 conn: database connection 173 174 Returns: 175 seller profile 176 177 Raises: 178 HTTPException: if seller not found 179 """ 180 seller_profile = await SellerQuerier(conn).get_seller(user_id=seller_id) 181 if not seller_profile: 182 raise HTTPException( 183 status_code=status.HTTP_404_NOT_FOUND, detail="Seller not found" 184 ) 185 return seller_profile
Get seller profile by ID.
Arguments:
- seller_id: unique identifier of the seller
- conn: database connection
Returns:
seller profile
Raises:
- HTTPException: if seller not found
188@router.post( 189 "", 190 status_code=status.HTTP_201_CREATED, 191 summary="Register seller", 192 description="Creates a new seller and their corresponding user entity.", 193) 194async def register_seller(form: CreateSellerForm, conn: database_dependency) -> None: 195 """Creates seller and coressponding user. 196 197 Args: 198 form: signup form from user 199 conn: database connection 200 """ 201 _ = await create_seller(form, conn)
Creates seller and coressponding user.
Arguments:
- form: signup form from user
- conn: database connection
204class UpdateSellerForm(BaseModel): 205 """Form for updating seller profile.""" 206 207 address_line1: str 208 address_line2: str | None = None 209 city: str 210 post_code: str 211 region: str | None = None 212 country: str 213 latitude: float | None = None 214 longitude: float | None = None
Form for updating seller profile.
217@router.patch( 218 "/me", 219 status_code=status.HTTP_200_OK, 220 summary="Update seller profile", 221 description="Updates the profile information for the authenticated seller.", 222) 223async def update_seller( 224 form: UpdateSellerForm, conn: database_dependency, seller: SellerAuthDep 225) -> None: 226 """Update seller profile. 227 228 Args: 229 form: seller update form 230 conn: database connection 231 seller: seller session 232 233 Raises: 234 HTTPException: if failed to update seller 235 """ 236 seller_querier = SellerQuerier(conn) 237 seller_record = await seller_querier.get_seller(user_id=seller.user_id) 238 if seller_record is None: 239 raise HTTPException( 240 status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to get seller" 241 ) 242 updated_seller = await seller_querier.update_seller( 243 UpdateSellerParams( 244 user_id=seller.user_id, 245 seller_name=seller_record.seller_name, 246 address_line1=form.address_line1, 247 address_line2=form.address_line2, 248 city=form.city, 249 post_code=form.post_code, 250 region=form.region, 251 country=form.country, 252 latitude=form.latitude, 253 longitude=form.longitude, 254 ) 255 ) 256 if not updated_seller: 257 raise HTTPException( 258 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 259 detail="Failed to update seller", 260 )
Update seller profile.
Arguments:
- form: seller update form
- conn: database connection
- seller: seller session
Raises:
- HTTPException: if failed to update seller
263class BundleForm(BaseModel): 264 """User form for bundles.""" 265 266 bundle_name: str 267 description: str 268 total_qty: int 269 price: Decimal = Field(decimal_places=2, gt=0) 270 discount_percentage: int = Field(lt=100, gt=0) 271 weight: int 272 categories: list[int] = Field(min_length=1) 273 allergens: list[int] 274 window_start: datetime 275 window_end: datetime
User form for bundles.
278@router.post( 279 "/me/bundles", 280 tags=["bundles"], 281 status_code=status.HTTP_201_CREATED, 282 summary="Create bundle", 283 description="Creates a new bundle for the authenticated seller.", 284) 285async def create_bundle( 286 form: BundleForm, conn: database_dependency, seller: SellerAuthDep 287) -> Bundle: 288 """Create bundle. 289 290 Args: 291 form: bundle info form 292 conn: database connection 293 seller: sellers session 294 295 Returns: 296 created bundle 297 298 Raises: 299 HTTPException: if failed to create bundle 300 """ 301 category_querier = CategoryQuerier(conn) 302 allergen_querier = AllergenQuerier(conn) 303 coefficients: list[float] = [] 304 for category in form.categories: 305 if ( 306 category_record := await category_querier.get_category(category_id=category) 307 ) is None: 308 raise HTTPException( 309 status.HTTP_500_INTERNAL_SERVER_ERROR, 310 "Failed to get category coefficient", 311 ) 312 coefficients.append(category_record.category_coefficient) 313 carbon_dioxide = estimate_carbon_doixide_saved(coefficients, weight_g=form.weight) 314 bundle = await BundleQuerier(conn).create_bundle( 315 CreateBundleParams( 316 seller_id=seller.user_id, 317 bundle_name=form.bundle_name, 318 description=form.description, 319 total_qty=form.total_qty, 320 price=form.price, 321 carbon_dioxide=carbon_dioxide, 322 discount_percentage=form.discount_percentage, 323 window_start=form.window_start, 324 window_end=form.window_end, 325 ) 326 ) 327 if not bundle: 328 raise HTTPException( 329 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 330 detail="Failed to create bundle", 331 ) 332 for category in form.categories: 333 bundle_category = await category_querier.add_bundles_category( 334 AddBundlesCategoryParams(bundle_id=bundle.bundle_id, category_id=category) 335 ) 336 if bundle_category is None: 337 raise HTTPException( 338 status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to add bundle category" 339 ) 340 for allergen in form.allergens: 341 bundle_allergen = await allergen_querier.add_bundles_allergen( 342 AddBundlesAllergenParams(bundle_id=bundle.bundle_id, allergen_id=allergen) 343 ) 344 if bundle_allergen is None: 345 raise HTTPException( 346 status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to add bundle allergen" 347 ) 348 return bundle
Create bundle.
Arguments:
- form: bundle info form
- conn: database connection
- seller: sellers session
Returns:
created bundle
Raises:
- HTTPException: if failed to create bundle
351async def update_bundle_categories( 352 bundle_id: int, category_querier: CategoryQuerier, form: BundleForm 353) -> None: 354 """Update bundle categories to a new once. 355 356 Args: 357 bundle_id: bundle id 358 category_querier: categories async querier 359 form: update bundle form 360 361 Raises: 362 HTTPException: if failed to update categories 363 """ 364 bundle_categories = [ 365 bundle_category 366 async for bundle_category in category_querier.get_bundle_categories( 367 bundle_id=bundle_id 368 ) 369 ] 370 for category in form.categories: 371 if category not in bundle_categories: 372 added_category = await category_querier.add_bundles_category( 373 AddBundlesCategoryParams(bundle_id=bundle_id, category_id=category) 374 ) 375 if added_category is None: 376 raise HTTPException( 377 status.HTTP_500_INTERNAL_SERVER_ERROR, 378 "Failed to add bundle category", 379 ) 380 for bundle_category in bundle_categories: 381 if bundle_category not in form.categories: 382 deleted_category = await category_querier.delete_bundle_category( 383 DeleteBundleCategoryParams( 384 bundle_id=bundle_id, category_id=bundle_category 385 ) 386 ) 387 if deleted_category is None: 388 raise HTTPException( 389 status.HTTP_500_INTERNAL_SERVER_ERROR, 390 "failed to delete bundle category", 391 )
Update bundle categories to a new once.
Arguments:
- bundle_id: bundle id
- category_querier: categories async querier
- form: update bundle form
Raises:
- HTTPException: if failed to update categories
394async def update_bundle_allergens( 395 bundle_id: int, allergen_querier: AllergenQuerier, form: BundleForm 396) -> None: 397 """Update bandle allergens to a new once. 398 399 Args: 400 bundle_id: bundle id 401 allergen_querier: allergen async querier 402 form: bundle update form 403 404 Raises: 405 HTTPException: if failed to update allergens 406 """ 407 bundle_allergens = [ 408 bundle_allergen 409 async for bundle_allergen in allergen_querier.get_bundle_allergens( 410 bundle_id=bundle_id 411 ) 412 ] 413 for allergen in form.allergens: 414 if allergen not in bundle_allergens: 415 added_allergen = await allergen_querier.add_bundles_allergen( 416 AddBundlesAllergenParams(bundle_id=bundle_id, allergen_id=allergen) 417 ) 418 if added_allergen is None: 419 raise HTTPException( 420 status.HTTP_500_INTERNAL_SERVER_ERROR, 421 "Failed to add bundle allergen", 422 ) 423 for bundle_allergen in bundle_allergens: 424 if bundle_allergen not in form.allergens: 425 deleted_allergen = await allergen_querier.delete_bundle_allergen( 426 DeleteBundleAllergenParams( 427 bundle_id=bundle_id, allergen_id=bundle_allergen 428 ) 429 ) 430 if deleted_allergen is None: 431 raise HTTPException( 432 status.HTTP_500_INTERNAL_SERVER_ERROR, 433 "failed to delete bundle allergen", 434 )
Update bandle allergens to a new once.
Arguments:
- bundle_id: bundle id
- allergen_querier: allergen async querier
- form: bundle update form
Raises:
- HTTPException: if failed to update allergens
437@router.patch( 438 "/me/bundles/{bundle_id}", 439 tags=["bundles"], 440 status_code=status.HTTP_200_OK, 441 summary="Update bundle", 442 description="Updates an existing bundle for the authenticated seller.", 443) 444async def update_bundle( 445 bundle_id: int, form: BundleForm, conn: database_dependency, seller: SellerAuthDep 446) -> Bundle: 447 """Update bundle. 448 449 Args: 450 bundle_id: bundle id 451 form: updated bundle info form 452 conn: database connection 453 seller: sellers session 454 455 Returns: 456 updated bundle 457 458 Raises: 459 HTTPException: if failed to update bundle 460 """ 461 category_querier = CategoryQuerier(conn) 462 allergen_querier = AllergenQuerier(conn) 463 coefficients: list[float] = [] 464 for category in form.categories: 465 if ( 466 category_record := await category_querier.get_category(category_id=category) 467 ) is None: 468 raise HTTPException( 469 status.HTTP_500_INTERNAL_SERVER_ERROR, 470 "Failed to get category coefficient", 471 ) 472 coefficients.append(category_record.category_coefficient) 473 carbon_dioxide = estimate_carbon_doixide_saved(coefficients, weight_g=form.weight) 474 bundle = await BundleQuerier(conn).update_bundle( 475 UpdateBundleParams( 476 bundle_id=bundle_id, 477 seller_id=seller.user_id, 478 bundle_name=form.bundle_name, 479 description=form.description, 480 total_qty=form.total_qty, 481 price=form.price, 482 discount_percentage=form.discount_percentage, 483 window_start=form.window_start, 484 window_end=form.window_end, 485 carbon_dioxide=carbon_dioxide, 486 ) 487 ) 488 if not bundle: 489 raise HTTPException( 490 status_code=status.HTTP_404_NOT_FOUND, 491 detail="Bundle not found or not owned by seller", 492 ) 493 await update_bundle_categories(bundle_id, category_querier, form) 494 await update_bundle_allergens(bundle_id, allergen_querier, form) 495 return bundle
Update bundle.
Arguments:
- bundle_id: bundle id
- form: updated bundle info form
- conn: database connection
- seller: sellers session
Returns:
updated bundle
Raises:
- HTTPException: if failed to update bundle
498@router.get( 499 "/me/bundles", 500 status_code=status.HTTP_200_OK, 501 summary="Get seller bundles", 502 description="Retrieves all bundles created by the authenticated seller.", 503) 504async def get_bundles(conn: database_dependency, seller: SellerAuthDep) -> list[Bundle]: 505 """Get sellers bundles. 506 507 Args: 508 conn: database connection 509 seller: sellers connection 510 511 Returns: 512 list of sellers bundles 513 514 Raises: 515 HTTPException: if failed to get bundles 516 """ 517 bundles = [ 518 item 519 async for item in BundleQuerier(conn).get_sellers_bundles( 520 seller_id=seller.user_id 521 ) 522 ] 523 if bundles is None: 524 raise HTTPException( 525 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 526 detail="Failed to get bundles", 527 ) 528 return list(bundles)
Get sellers bundles.
Arguments:
- conn: database connection
- seller: sellers connection
Returns:
list of sellers bundles
Raises:
- HTTPException: if failed to get bundles
531@router.get( 532 "/me/bundles/{bundle_id}/reservations", 533 tags=["reservations"], 534 status_code=status.HTTP_200_OK, 535 summary="Get bundle reservations", 536 description=( 537 "Retrieves all reservations for a specific bundle owned " 538 "by the authenticated seller." 539 ), 540) 541async def get_reservations( 542 bundle_id: int, conn: database_dependency, seller: SellerAuthDep 543) -> list[Reservation]: 544 """Get reservations for sellers bundle. 545 546 Args: 547 bundle_id: bundle id 548 conn: database connection 549 seller: sellers session 550 551 Returns: 552 list of reservations for specific bundle 553 554 Raises: 555 HTTPException: if failed to get reservations 556 """ 557 bundle = await BundleQuerier(conn).get_sellers_bundle( 558 GetSellersBundleParams(bundle_id=bundle_id, seller_id=seller.user_id) 559 ) 560 if not bundle: 561 raise HTTPException( 562 status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found" 563 ) 564 reservations = [ 565 item 566 async for item in ReservationsQuerier(conn).get_bundle_reservations( 567 bundle_id=bundle.bundle_id 568 ) 569 ] 570 if reservations is None: 571 raise HTTPException( 572 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 573 detail="Failed to find bundle reservations", 574 ) 575 return list(reservations)
Get reservations for sellers bundle.
Arguments:
- bundle_id: bundle id
- conn: database connection
- seller: sellers session
Returns:
list of reservations for specific bundle
Raises:
- HTTPException: if failed to get reservations
578@router.patch( 579 "/me/bundles/{bundle_id}/reservations/collect", 580 tags=["reservations"], 581 status_code=status.HTTP_200_OK, 582 summary="Collect reservation", 583 description="Confirms the collection of a reservation using a claim code.", 584) 585async def reservation_collection( 586 bundle_id: int, 587 claim_code: str, 588 conn: database_dependency, 589 seller: SellerAuthDep, 590 badge_engine: Annotated[BadgeEngine, Depends(BadgeEngine)], 591) -> Reservation: 592 """Confirm reservation collection. 593 594 Args: 595 bundle_id: bundle_id 596 claim_code: claim code 597 conn: database connection 598 seller: sellers session 599 badge_engine: badge acquiry engine 600 601 Returns: 602 confirmed claimed reservation 603 604 Raises: 605 HTTPException: if failed to collect reservation 606 """ 607 reservation_querier = ReservationsQuerier(conn) 608 reservation = await reservation_querier.get_reservation_collection( 609 GetReservationCollectionParams(bundle_id=bundle_id, claim_code=claim_code) 610 ) 611 if not reservation: 612 raise HTTPException( 613 status_code=status.HTTP_404_NOT_FOUND, detail="Reservation not found" 614 ) 615 bundle = await BundleQuerier(conn).get_bundle(bundle_id=reservation.bundle_id) 616 if not bundle: 617 raise HTTPException( 618 status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found" 619 ) 620 if bundle.seller_id != seller.user_id: 621 raise HTTPException( 622 status_code=status.HTTP_403_FORBIDDEN, 623 detail="Reservation does not belong to your bundle", 624 ) 625 claimed_reservation = await reservation_querier.collect_reservation( 626 reservation_id=reservation.reservation_id 627 ) 628 if not claimed_reservation: 629 raise HTTPException( 630 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 631 detail="Failed to update reservation status", 632 ) 633 await conn.commit() 634 badge_engine.run(claimed_reservation.consumer_id, bundle.window_start) 635 return claimed_reservation
Confirm reservation collection.
Arguments:
- bundle_id: bundle_id
- claim_code: claim code
- conn: database connection
- seller: sellers session
- badge_engine: badge acquiry engine
Returns:
confirmed claimed reservation
Raises:
- HTTPException: if failed to collect reservation
638@router.post( 639 "/me/analytics", 640 tags=["analytics"], 641 summary="Refresh analytics", 642 description="Triggers a refresh of analytics graphs for the authenticated seller.", 643) 644async def refresh_analytics( 645 seller: SellerAuthDep, 646 analytics_processer: Annotated[AnalyticsProcesser, Depends(AnalyticsProcesser)], 647) -> None: 648 """Refresh analytics graphs for the authenticated seller. 649 650 Args: 651 seller: authenticated seller session 652 analytics_processer: analytics processing engine 653 """ 654 analytics_processer.run(seller.user_id)
Refresh analytics graphs for the authenticated seller.
Arguments:
- seller: authenticated seller session
- analytics_processer: analytics processing engine
657@router.patch( 658 path="/me/bundles/{bundle_id}/image", 659 status_code=status.HTTP_202_ACCEPTED, 660 summary="Change bundle image", 661 description="Updates the image for a bundle owned by the authenticated seller.", 662) 663async def change_bundle_image( 664 bundle_id: int, file: UploadFile, conn: database_dependency, seller: SellerAuthDep 665) -> None: 666 """Change bundle image. 667 668 Args: 669 bundle_id: bundle id 670 file: bundle image 671 conn: database connection 672 seller: seller session 673 674 Raises: 675 HTTPException: if failed to change image 676 """ 677 if ( 678 BundleQuerier(conn).get_sellers_bundle( 679 GetSellersBundleParams(seller_id=seller.user_id, bundle_id=bundle_id) 680 ) 681 is None 682 ): 683 raise HTTPException(status.HTTP_404_NOT_FOUND, "bundle not found") 684 await block_management.upload_bundle_image(bundle_id, file)
Change bundle image.
Arguments:
- bundle_id: bundle id
- file: bundle image
- conn: database connection
- seller: seller session
Raises:
- HTTPException: if failed to change image
687@router.get( 688 path="/me/bundles/{bundle_id}/image", 689 status_code=status.HTTP_200_OK, 690 summary="Get bundle image", 691 description="Retrieves the image for a bundle owned by the authenticated seller.", 692) 693async def get_bundle_image( 694 bundle_id: int, conn: database_dependency, seller: SellerAuthDep 695) -> Response: 696 """Get bundle image. 697 698 Args: 699 bundle_id: bundle id 700 conn: database connection 701 seller: seller session 702 703 Returns: 704 bundle image 705 706 Raises: 707 HTTPException: if failed to get bundle image 708 """ 709 if ( 710 BundleQuerier(conn).get_sellers_bundle( 711 GetSellersBundleParams(seller_id=seller.user_id, bundle_id=bundle_id) 712 ) 713 is None 714 ): 715 raise HTTPException(status.HTTP_404_NOT_FOUND, "bundle not found") 716 return Response( 717 block_management.get_bundle_image(bundle_id), media_type="image/jpeg" 718 )
Get bundle image.
Arguments:
- bundle_id: bundle id
- conn: database connection
- seller: seller session
Returns:
bundle image
Raises:
- HTTPException: if failed to get bundle image
721@router.get( 722 "/me/analytics", 723 status_code=status.HTTP_200_OK, 724 tags=["analytics"], 725 summary="Get analytics graph types", 726 description="Retrieves all available analytics graph types for the seller.", 727) 728async def get_analytics_graph_types( 729 conn: database_dependency, _: SellerAuthDep 730) -> list[AnalyticsGraphsType]: 731 """Get all graph types. 732 733 Args: 734 conn: database connection 735 736 Returns: 737 list of all graph types 738 """ 739 return [ 740 graph_type async for graph_type in AnalyticsQuerier(conn).get_graphs_types() 741 ]
Get all graph types.
Arguments:
- conn: database connection
Returns:
list of all graph types
744@router.get( 745 "/me/analytics/{graph_type_id}", 746 status_code=status.HTTP_200_OK, 747 tags=["analytics"], 748 summary="Get analytics graph", 749 description="Retrieves an analytics graph with series and points for the seller.", 750) 751async def get_analytics_graph( 752 graph_type_id: int, conn: database_dependency, seller: SellerAuthDep 753) -> tuple[AnalyticsGraph, list[tuple[AnalyticsSeries, list[AnalyticsPoint]]]]: 754 """Get analytics graph. 755 756 Args: 757 graph_type_id: graph type id 758 conn: database connection 759 seller: seller session 760 761 Returns: 762 graph, series and points 763 764 Raises: 765 HTTPException: if failed to get graph 766 """ 767 analytics_querier = AnalyticsQuerier(conn) 768 if (await analytics_querier.get_graph_type(graph_type_id=graph_type_id)) is None: 769 raise HTTPException(status.HTTP_404_NOT_FOUND, "failed to find graph type") 770 if ( 771 graph := await analytics_querier.get_graph( 772 GetGraphParams(seller_id=seller.user_id, graph_type=graph_type_id) 773 ) 774 ) is None: 775 raise HTTPException( 776 status.HTTP_500_INTERNAL_SERVER_ERROR, "failed to get graph" 777 ) 778 series = [ 779 series 780 async for series in analytics_querier.get_graph_series(graph_id=graph.graph_id) 781 ] 782 if len(series) == 0: 783 return (graph, []) 784 series_and_points: list[tuple[AnalyticsSeries, list[AnalyticsPoint]]] = [] 785 for single_series in series: 786 single_series_points: list[AnalyticsPoint] = [ 787 point 788 async for point in analytics_querier.get_graph_points( 789 series_id=single_series.series_id 790 ) 791 ] 792 series_and_points.append((single_series, single_series_points)) 793 return (graph, series_and_points)
Get analytics graph.
Arguments:
- graph_type_id: graph type id
- conn: database connection
- seller: seller session
Returns:
graph, series and points
Raises:
- HTTPException: if failed to get graph
796@router.get( 797 "/me/bundles/{bundle_id}/forecasting", 798 status_code=status.HTTP_200_OK, 799 tags=["forecasts"], 800 summary="Get bundle forecast", 801 description="Retrieves a forecast for a specific bundle.", 802) 803async def get_bundle_forecast( 804 bundle_id: int, conn: database_dependency, seller: SellerAuthDep 805) -> ForecastOutput: 806 """Get forecast for a specific bundle. 807 808 Args: 809 bundle_id: bundle id 810 conn: database connection 811 seller: seller session 812 813 Returns: 814 forecast for the bundle 815 816 Raises: 817 HTTPException: if forecast not found 818 """ 819 forecast = await ForecastQuerier(conn).get_forecast_output_by_bundle( 820 bundle_id=bundle_id 821 ) 822 if forecast is None: 823 raise HTTPException(status.HTTP_404_NOT_FOUND, "forecast not found") 824 if forecast.seller_id != seller.user_id: 825 raise HTTPException(status.HTTP_403_FORBIDDEN, "not your forecast") 826 return forecast
Get forecast for a specific bundle.
Arguments:
- bundle_id: bundle id
- conn: database connection
- seller: seller session
Returns:
forecast for the bundle
Raises:
- HTTPException: if forecast not found