b1sl-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- b1sl/__init__.py +5 -0
- b1sl/b1sl/__init__.py +56 -0
- b1sl/b1sl/_types.py +28 -0
- b1sl/b1sl/adapter.py +40 -0
- b1sl/b1sl/adapter_protocol.py +28 -0
- b1sl/b1sl/async_client.py +268 -0
- b1sl/b1sl/async_rest_adapter.py +347 -0
- b1sl/b1sl/base_adapter.py +394 -0
- b1sl/b1sl/client.py +112 -0
- b1sl/b1sl/config.py +138 -0
- b1sl/b1sl/config_manager.py +77 -0
- b1sl/b1sl/contrib/__init__.py +14 -0
- b1sl/b1sl/contrib/django/__init__.py +13 -0
- b1sl/b1sl/contrib/django/middleware/__init__.py +27 -0
- b1sl/b1sl/contrib/django/middleware/base.py +89 -0
- b1sl/b1sl/contrib/django/middleware/odata_decode_middleware.py +20 -0
- b1sl/b1sl/contrib/django/middleware/odata_transform_url_middleware.py +20 -0
- b1sl/b1sl/entities/__init__.py +2209 -0
- b1sl/b1sl/environment.py +36 -0
- b1sl/b1sl/exceptions/exceptions.py +142 -0
- b1sl/b1sl/fields/__init__.py +2020 -0
- b1sl/b1sl/fields/_generated/__init__.py +0 -0
- b1sl/b1sl/fields/_generated/complex_types.py +6926 -0
- b1sl/b1sl/fields/_generated/entities/__init__.py +0 -0
- b1sl/b1sl/fields/_generated/entities/businesspartners.py +544 -0
- b1sl/b1sl/fields/_generated/entities/finance.py +451 -0
- b1sl/b1sl/fields/_generated/entities/general.py +4587 -0
- b1sl/b1sl/fields/_generated/entities/inventory.py +761 -0
- b1sl/b1sl/fields/_generated/entities/production.py +256 -0
- b1sl/b1sl/fields/_generated/entities/purchasing.py +53 -0
- b1sl/b1sl/fields/_generated/entities/sales.py +244 -0
- b1sl/b1sl/logging_utils.py +119 -0
- b1sl/b1sl/models/__init__.py +13 -0
- b1sl/b1sl/models/_generated/__init__.py +0 -0
- b1sl/b1sl/models/_generated/_types.py +5 -0
- b1sl/b1sl/models/_generated/complex_types.py +6929 -0
- b1sl/b1sl/models/_generated/entities/__init__.py +581 -0
- b1sl/b1sl/models/_generated/entities/businesspartners.py +565 -0
- b1sl/b1sl/models/_generated/entities/finance.py +472 -0
- b1sl/b1sl/models/_generated/entities/general.py +4831 -0
- b1sl/b1sl/models/_generated/entities/inventory.py +786 -0
- b1sl/b1sl/models/_generated/entities/production.py +272 -0
- b1sl/b1sl/models/_generated/entities/purchasing.py +64 -0
- b1sl/b1sl/models/_generated/entities/sales.py +263 -0
- b1sl/b1sl/models/_generated/enums.py +3501 -0
- b1sl/b1sl/models/_generated/resources/__init__.py +0 -0
- b1sl/b1sl/models/_overrides/inventory.py +24 -0
- b1sl/b1sl/models/base.py +209 -0
- b1sl/b1sl/models/odata_query_model.py +50 -0
- b1sl/b1sl/models/paginated_result.py +85 -0
- b1sl/b1sl/models/result.py +17 -0
- b1sl/b1sl/resources/__init__.py +11 -0
- b1sl/b1sl/resources/_generated/__init__.py +0 -0
- b1sl/b1sl/resources/_generated/account_category.py +15 -0
- b1sl/b1sl/resources/_generated/account_category_service.py +21 -0
- b1sl/b1sl/resources/_generated/account_segmentation_categories.py +15 -0
- b1sl/b1sl/resources/_generated/account_segmentations.py +15 -0
- b1sl/b1sl/resources/_generated/accounts_service.py +33 -0
- b1sl/b1sl/resources/_generated/accrual_types.py +15 -0
- b1sl/b1sl/resources/_generated/accrual_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/activities.py +15 -0
- b1sl/b1sl/resources/_generated/activities_service.py +109 -0
- b1sl/b1sl/resources/_generated/activity_locations.py +15 -0
- b1sl/b1sl/resources/_generated/activity_recipient_lists.py +15 -0
- b1sl/b1sl/resources/_generated/activity_recipient_lists_service.py +21 -0
- b1sl/b1sl/resources/_generated/activity_statuses.py +15 -0
- b1sl/b1sl/resources/_generated/activity_types.py +15 -0
- b1sl/b1sl/resources/_generated/additional_expenses.py +15 -0
- b1sl/b1sl/resources/_generated/address_service.py +38 -0
- b1sl/b1sl/resources/_generated/alert_managements.py +16 -0
- b1sl/b1sl/resources/_generated/alternate_cat_num.py +15 -0
- b1sl/b1sl/resources/_generated/alternative_items_service.py +113 -0
- b1sl/b1sl/resources/_generated/approval_requests.py +15 -0
- b1sl/b1sl/resources/_generated/approval_requests_service.py +33 -0
- b1sl/b1sl/resources/_generated/approval_stages.py +15 -0
- b1sl/b1sl/resources/_generated/approval_stages_service.py +21 -0
- b1sl/b1sl/resources/_generated/approval_templates.py +15 -0
- b1sl/b1sl/resources/_generated/approval_templates_service.py +21 -0
- b1sl/b1sl/resources/_generated/asset_capitalization.py +15 -0
- b1sl/b1sl/resources/_generated/asset_capitalization_credit_memo.py +15 -0
- b1sl/b1sl/resources/_generated/asset_capitalization_credit_memo_service.py +37 -0
- b1sl/b1sl/resources/_generated/asset_capitalization_service.py +37 -0
- b1sl/b1sl/resources/_generated/asset_classes.py +15 -0
- b1sl/b1sl/resources/_generated/asset_classes_service.py +21 -0
- b1sl/b1sl/resources/_generated/asset_depreciation_groups.py +15 -0
- b1sl/b1sl/resources/_generated/asset_depreciation_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/asset_groups.py +15 -0
- b1sl/b1sl/resources/_generated/asset_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/asset_manual_depreciation.py +15 -0
- b1sl/b1sl/resources/_generated/asset_manual_depreciation_service.py +37 -0
- b1sl/b1sl/resources/_generated/asset_retirement.py +15 -0
- b1sl/b1sl/resources/_generated/asset_retirement_service.py +37 -0
- b1sl/b1sl/resources/_generated/asset_transfer.py +15 -0
- b1sl/b1sl/resources/_generated/asset_transfer_service.py +37 -0
- b1sl/b1sl/resources/_generated/attachments2.py +15 -0
- b1sl/b1sl/resources/_generated/attribute_groups.py +15 -0
- b1sl/b1sl/resources/_generated/attribute_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/bank_charges_allocation_codes.py +15 -0
- b1sl/b1sl/resources/_generated/bank_charges_allocation_codes_service.py +21 -0
- b1sl/b1sl/resources/_generated/bank_pages.py +15 -0
- b1sl/b1sl/resources/_generated/bank_statements.py +15 -0
- b1sl/b1sl/resources/_generated/bank_statements_service.py +32 -0
- b1sl/b1sl/resources/_generated/banks.py +15 -0
- b1sl/b1sl/resources/_generated/bar_codes.py +15 -0
- b1sl/b1sl/resources/_generated/bar_codes_service.py +21 -0
- b1sl/b1sl/resources/_generated/batch_number_details.py +15 -0
- b1sl/b1sl/resources/_generated/bill_of_exchange_transactions.py +15 -0
- b1sl/b1sl/resources/_generated/bin_location_attributes.py +15 -0
- b1sl/b1sl/resources/_generated/bin_location_attributes_service.py +21 -0
- b1sl/b1sl/resources/_generated/bin_location_fields.py +15 -0
- b1sl/b1sl/resources/_generated/bin_location_fields_service.py +21 -0
- b1sl/b1sl/resources/_generated/bin_locations.py +15 -0
- b1sl/b1sl/resources/_generated/bin_locations_service.py +21 -0
- b1sl/b1sl/resources/_generated/blanket_agreements.py +15 -0
- b1sl/b1sl/resources/_generated/blanket_agreements_service.py +21 -0
- b1sl/b1sl/resources/_generated/boe_document_types.py +15 -0
- b1sl/b1sl/resources/_generated/boe_document_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/boe_instructions.py +15 -0
- b1sl/b1sl/resources/_generated/boe_instructions_service.py +21 -0
- b1sl/b1sl/resources/_generated/boe_lines_service.py +30 -0
- b1sl/b1sl/resources/_generated/boe_portfolios.py +15 -0
- b1sl/b1sl/resources/_generated/boe_portfolios_service.py +21 -0
- b1sl/b1sl/resources/_generated/bp_fiscal_registry_id.py +15 -0
- b1sl/b1sl/resources/_generated/bp_opening_balance_service.py +41 -0
- b1sl/b1sl/resources/_generated/bp_priorities.py +15 -0
- b1sl/b1sl/resources/_generated/branches.py +15 -0
- b1sl/b1sl/resources/_generated/branches_service.py +21 -0
- b1sl/b1sl/resources/_generated/brazil_beverage_indexers.py +15 -0
- b1sl/b1sl/resources/_generated/brazil_beverage_indexers_service.py +21 -0
- b1sl/b1sl/resources/_generated/brazil_fuel_indexers.py +15 -0
- b1sl/b1sl/resources/_generated/brazil_fuel_indexers_service.py +21 -0
- b1sl/b1sl/resources/_generated/brazil_multi_indexers.py +15 -0
- b1sl/b1sl/resources/_generated/brazil_numeric_indexers.py +15 -0
- b1sl/b1sl/resources/_generated/brazil_string_indexers.py +15 -0
- b1sl/b1sl/resources/_generated/budget_distributions.py +15 -0
- b1sl/b1sl/resources/_generated/budget_scenarios.py +15 -0
- b1sl/b1sl/resources/_generated/budgets.py +15 -0
- b1sl/b1sl/resources/_generated/business_partner_groups.py +15 -0
- b1sl/b1sl/resources/_generated/business_partner_properties.py +15 -0
- b1sl/b1sl/resources/_generated/business_partner_properties_service.py +21 -0
- b1sl/b1sl/resources/_generated/business_partners.py +15 -0
- b1sl/b1sl/resources/_generated/business_partners_service.py +47 -0
- b1sl/b1sl/resources/_generated/business_places.py +15 -0
- b1sl/b1sl/resources/_generated/campaign_response_type.py +15 -0
- b1sl/b1sl/resources/_generated/campaign_response_type_service.py +21 -0
- b1sl/b1sl/resources/_generated/campaigns.py +15 -0
- b1sl/b1sl/resources/_generated/campaigns_service.py +21 -0
- b1sl/b1sl/resources/_generated/cash_discounts.py +15 -0
- b1sl/b1sl/resources/_generated/cash_discounts_service.py +21 -0
- b1sl/b1sl/resources/_generated/cash_flow_line_items.py +15 -0
- b1sl/b1sl/resources/_generated/cash_flow_line_items_service.py +21 -0
- b1sl/b1sl/resources/_generated/certificate_series.py +15 -0
- b1sl/b1sl/resources/_generated/certificate_series_service.py +21 -0
- b1sl/b1sl/resources/_generated/change_logs_service.py +51 -0
- b1sl/b1sl/resources/_generated/chart_of_accounts.py +15 -0
- b1sl/b1sl/resources/_generated/check_lines_service.py +38 -0
- b1sl/b1sl/resources/_generated/checksfor_payment.py +15 -0
- b1sl/b1sl/resources/_generated/choose_from_list.py +15 -0
- b1sl/b1sl/resources/_generated/client_mixin.py +3920 -0
- b1sl/b1sl/resources/_generated/closing_date_procedure.py +15 -0
- b1sl/b1sl/resources/_generated/cockpits.py +15 -0
- b1sl/b1sl/resources/_generated/cockpits_service.py +46 -0
- b1sl/b1sl/resources/_generated/commission_groups.py +15 -0
- b1sl/b1sl/resources/_generated/company_service.py +410 -0
- b1sl/b1sl/resources/_generated/contacts.py +15 -0
- b1sl/b1sl/resources/_generated/contract_templates.py +15 -0
- b1sl/b1sl/resources/_generated/correction_invoice.py +15 -0
- b1sl/b1sl/resources/_generated/correction_invoice_reversal.py +15 -0
- b1sl/b1sl/resources/_generated/correction_invoice_reversal_service.py +65 -0
- b1sl/b1sl/resources/_generated/correction_invoice_service.py +78 -0
- b1sl/b1sl/resources/_generated/correction_purchase_invoice.py +15 -0
- b1sl/b1sl/resources/_generated/correction_purchase_invoice_reversal.py +15 -0
- b1sl/b1sl/resources/_generated/correction_purchase_invoice_reversal_service.py +65 -0
- b1sl/b1sl/resources/_generated/correction_purchase_invoice_service.py +78 -0
- b1sl/b1sl/resources/_generated/cost_center_types.py +15 -0
- b1sl/b1sl/resources/_generated/cost_center_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/cost_element_service.py +21 -0
- b1sl/b1sl/resources/_generated/cost_elements.py +15 -0
- b1sl/b1sl/resources/_generated/counties.py +15 -0
- b1sl/b1sl/resources/_generated/counties_service.py +21 -0
- b1sl/b1sl/resources/_generated/countries.py +15 -0
- b1sl/b1sl/resources/_generated/countries_service.py +21 -0
- b1sl/b1sl/resources/_generated/credit_card_payments.py +15 -0
- b1sl/b1sl/resources/_generated/credit_cards.py +15 -0
- b1sl/b1sl/resources/_generated/credit_lines_service.py +38 -0
- b1sl/b1sl/resources/_generated/credit_notes.py +15 -0
- b1sl/b1sl/resources/_generated/credit_notes_service.py +100 -0
- b1sl/b1sl/resources/_generated/credit_payment_methods.py +15 -0
- b1sl/b1sl/resources/_generated/currencies.py +15 -0
- b1sl/b1sl/resources/_generated/customer_equipment_cards.py +15 -0
- b1sl/b1sl/resources/_generated/customs_declaration.py +15 -0
- b1sl/b1sl/resources/_generated/customs_groups.py +15 -0
- b1sl/b1sl/resources/_generated/cycle_count_determinations.py +15 -0
- b1sl/b1sl/resources/_generated/cycle_count_determinations_service.py +21 -0
- b1sl/b1sl/resources/_generated/dashboard_packages_service.py +28 -0
- b1sl/b1sl/resources/_generated/deductible_tax_service.py +21 -0
- b1sl/b1sl/resources/_generated/deductible_taxes.py +15 -0
- b1sl/b1sl/resources/_generated/deduction_tax_groups.py +15 -0
- b1sl/b1sl/resources/_generated/deduction_tax_hierarchies.py +15 -0
- b1sl/b1sl/resources/_generated/deduction_tax_sub_groups.py +15 -0
- b1sl/b1sl/resources/_generated/deduction_tax_sub_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/delivery_notes.py +15 -0
- b1sl/b1sl/resources/_generated/delivery_notes_service.py +85 -0
- b1sl/b1sl/resources/_generated/departments.py +15 -0
- b1sl/b1sl/resources/_generated/departments_service.py +21 -0
- b1sl/b1sl/resources/_generated/deposits.py +15 -0
- b1sl/b1sl/resources/_generated/deposits_service.py +53 -0
- b1sl/b1sl/resources/_generated/depreciation_areas.py +15 -0
- b1sl/b1sl/resources/_generated/depreciation_areas_service.py +21 -0
- b1sl/b1sl/resources/_generated/depreciation_type_pools.py +15 -0
- b1sl/b1sl/resources/_generated/depreciation_type_pools_service.py +21 -0
- b1sl/b1sl/resources/_generated/depreciation_types.py +15 -0
- b1sl/b1sl/resources/_generated/depreciation_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/determination_criterias.py +15 -0
- b1sl/b1sl/resources/_generated/determination_criterias_service.py +21 -0
- b1sl/b1sl/resources/_generated/dimensions.py +15 -0
- b1sl/b1sl/resources/_generated/dimensions_service.py +21 -0
- b1sl/b1sl/resources/_generated/distribution_rules.py +15 -0
- b1sl/b1sl/resources/_generated/distribution_rules_service.py +21 -0
- b1sl/b1sl/resources/_generated/dnf_code_setup.py +15 -0
- b1sl/b1sl/resources/_generated/dnf_code_setup_service.py +21 -0
- b1sl/b1sl/resources/_generated/down_payments.py +15 -0
- b1sl/b1sl/resources/_generated/down_payments_service.py +68 -0
- b1sl/b1sl/resources/_generated/drafts.py +15 -0
- b1sl/b1sl/resources/_generated/drafts_service.py +86 -0
- b1sl/b1sl/resources/_generated/dunning_letters.py +15 -0
- b1sl/b1sl/resources/_generated/dunning_terms.py +15 -0
- b1sl/b1sl/resources/_generated/dunning_terms_service.py +21 -0
- b1sl/b1sl/resources/_generated/dynamic_system_strings.py +15 -0
- b1sl/b1sl/resources/_generated/electronic_communication_action_service.py +122 -0
- b1sl/b1sl/resources/_generated/electronic_communication_actions_service.py +119 -0
- b1sl/b1sl/resources/_generated/electronic_file_formats.py +15 -0
- b1sl/b1sl/resources/_generated/electronic_file_formats_service.py +21 -0
- b1sl/b1sl/resources/_generated/email_groups.py +15 -0
- b1sl/b1sl/resources/_generated/email_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/employee_id_type.py +15 -0
- b1sl/b1sl/resources/_generated/employee_id_type_service.py +21 -0
- b1sl/b1sl/resources/_generated/employee_position.py +15 -0
- b1sl/b1sl/resources/_generated/employee_position_service.py +21 -0
- b1sl/b1sl/resources/_generated/employee_roles_setup.py +15 -0
- b1sl/b1sl/resources/_generated/employee_roles_setup_service.py +21 -0
- b1sl/b1sl/resources/_generated/employee_status.py +15 -0
- b1sl/b1sl/resources/_generated/employee_status_service.py +21 -0
- b1sl/b1sl/resources/_generated/employee_transfers.py +15 -0
- b1sl/b1sl/resources/_generated/employee_transfers_service.py +21 -0
- b1sl/b1sl/resources/_generated/employees_info.py +15 -0
- b1sl/b1sl/resources/_generated/employment_category_service.py +21 -0
- b1sl/b1sl/resources/_generated/employment_categorys.py +15 -0
- b1sl/b1sl/resources/_generated/enhanced_discount_groups.py +15 -0
- b1sl/b1sl/resources/_generated/enhanced_discount_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/exceptional_event_service.py +21 -0
- b1sl/b1sl/resources/_generated/exceptional_events.py +15 -0
- b1sl/b1sl/resources/_generated/extended_translations.py +15 -0
- b1sl/b1sl/resources/_generated/extended_translations_service.py +21 -0
- b1sl/b1sl/resources/_generated/external_calls_service.py +54 -0
- b1sl/b1sl/resources/_generated/external_reconciliations_service.py +104 -0
- b1sl/b1sl/resources/_generated/fa_account_determinations.py +15 -0
- b1sl/b1sl/resources/_generated/fa_account_determinations_service.py +21 -0
- b1sl/b1sl/resources/_generated/factoring_indicators.py +15 -0
- b1sl/b1sl/resources/_generated/financial_years.py +15 -0
- b1sl/b1sl/resources/_generated/financial_years_service.py +21 -0
- b1sl/b1sl/resources/_generated/fiscal_printer.py +15 -0
- b1sl/b1sl/resources/_generated/fiscal_printer_service.py +21 -0
- b1sl/b1sl/resources/_generated/fixed_asset_items_service.py +55 -0
- b1sl/b1sl/resources/_generated/form_preferences.py +15 -0
- b1sl/b1sl/resources/_generated/formatted_searches.py +15 -0
- b1sl/b1sl/resources/_generated/forms1099.py +15 -0
- b1sl/b1sl/resources/_generated/gl_account_advanced_rules.py +15 -0
- b1sl/b1sl/resources/_generated/gl_account_advanced_rules_service.py +21 -0
- b1sl/b1sl/resources/_generated/goods_return_request.py +15 -0
- b1sl/b1sl/resources/_generated/goods_return_request_service.py +69 -0
- b1sl/b1sl/resources/_generated/gov_pay_codes.py +15 -0
- b1sl/b1sl/resources/_generated/gov_pay_codes_service.py +21 -0
- b1sl/b1sl/resources/_generated/gt_is_service.py +28 -0
- b1sl/b1sl/resources/_generated/holidays.py +15 -0
- b1sl/b1sl/resources/_generated/house_bank_accounts.py +15 -0
- b1sl/b1sl/resources/_generated/incoming_payments.py +15 -0
- b1sl/b1sl/resources/_generated/india_hsn.py +15 -0
- b1sl/b1sl/resources/_generated/india_hsn_service.py +21 -0
- b1sl/b1sl/resources/_generated/industries.py +15 -0
- b1sl/b1sl/resources/_generated/integration_packages_configure.py +15 -0
- b1sl/b1sl/resources/_generated/integration_packages_configure_service.py +21 -0
- b1sl/b1sl/resources/_generated/internal_reconciliations.py +15 -0
- b1sl/b1sl/resources/_generated/internal_reconciliations_service.py +51 -0
- b1sl/b1sl/resources/_generated/intrastat_configuration.py +15 -0
- b1sl/b1sl/resources/_generated/intrastat_configuration_service.py +21 -0
- b1sl/b1sl/resources/_generated/inventory_countings.py +15 -0
- b1sl/b1sl/resources/_generated/inventory_countings_service.py +21 -0
- b1sl/b1sl/resources/_generated/inventory_cycles.py +15 -0
- b1sl/b1sl/resources/_generated/inventory_gen_entries.py +15 -0
- b1sl/b1sl/resources/_generated/inventory_gen_entry_service.py +67 -0
- b1sl/b1sl/resources/_generated/inventory_gen_exit_service.py +66 -0
- b1sl/b1sl/resources/_generated/inventory_gen_exits.py +15 -0
- b1sl/b1sl/resources/_generated/inventory_opening_balances.py +15 -0
- b1sl/b1sl/resources/_generated/inventory_opening_balances_service.py +21 -0
- b1sl/b1sl/resources/_generated/inventory_postings.py +15 -0
- b1sl/b1sl/resources/_generated/inventory_postings_service.py +34 -0
- b1sl/b1sl/resources/_generated/inventory_transfer_requests.py +15 -0
- b1sl/b1sl/resources/_generated/inventory_transfer_requests_service.py +43 -0
- b1sl/b1sl/resources/_generated/invoices.py +15 -0
- b1sl/b1sl/resources/_generated/invoices_service.py +100 -0
- b1sl/b1sl/resources/_generated/item_groups.py +15 -0
- b1sl/b1sl/resources/_generated/item_properties.py +15 -0
- b1sl/b1sl/resources/_generated/items.py +15 -0
- b1sl/b1sl/resources/_generated/journal_entries.py +15 -0
- b1sl/b1sl/resources/_generated/journal_entry_document_type_service.py +21 -0
- b1sl/b1sl/resources/_generated/journal_entry_document_types.py +15 -0
- b1sl/b1sl/resources/_generated/journal_vouchers_service.py +45 -0
- b1sl/b1sl/resources/_generated/knowledge_base_solutions.py +15 -0
- b1sl/b1sl/resources/_generated/kp_is.py +15 -0
- b1sl/b1sl/resources/_generated/kp_is_service.py +21 -0
- b1sl/b1sl/resources/_generated/landed_costs.py +15 -0
- b1sl/b1sl/resources/_generated/landed_costs_codes.py +15 -0
- b1sl/b1sl/resources/_generated/landed_costs_service.py +21 -0
- b1sl/b1sl/resources/_generated/legal_data.py +15 -0
- b1sl/b1sl/resources/_generated/length_measures.py +15 -0
- b1sl/b1sl/resources/_generated/license_service.py +21 -0
- b1sl/b1sl/resources/_generated/local_era.py +15 -0
- b1sl/b1sl/resources/_generated/manufacturers.py +15 -0
- b1sl/b1sl/resources/_generated/material_groups.py +15 -0
- b1sl/b1sl/resources/_generated/material_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/material_revaluation.py +15 -0
- b1sl/b1sl/resources/_generated/material_revaluation_fifo_service.py +33 -0
- b1sl/b1sl/resources/_generated/material_revaluation_snb_service.py +33 -0
- b1sl/b1sl/resources/_generated/messages.py +15 -0
- b1sl/b1sl/resources/_generated/messages_service.py +35 -0
- b1sl/b1sl/resources/_generated/mobile_add_on_setting.py +15 -0
- b1sl/b1sl/resources/_generated/mobile_add_on_setting_service.py +21 -0
- b1sl/b1sl/resources/_generated/mobile_app_service.py +240 -0
- b1sl/b1sl/resources/_generated/multi_language_translations.py +15 -0
- b1sl/b1sl/resources/_generated/nature_of_assessees.py +15 -0
- b1sl/b1sl/resources/_generated/nature_of_assessees_service.py +21 -0
- b1sl/b1sl/resources/_generated/ncm_codes_setup.py +15 -0
- b1sl/b1sl/resources/_generated/ncm_codes_setup_service.py +21 -0
- b1sl/b1sl/resources/_generated/nf_models.py +15 -0
- b1sl/b1sl/resources/_generated/nf_models_service.py +21 -0
- b1sl/b1sl/resources/_generated/nf_tax_categories.py +15 -0
- b1sl/b1sl/resources/_generated/nf_tax_categories_service.py +21 -0
- b1sl/b1sl/resources/_generated/nota_fiscal_cfop.py +15 -0
- b1sl/b1sl/resources/_generated/nota_fiscal_cst.py +15 -0
- b1sl/b1sl/resources/_generated/nota_fiscal_usage.py +15 -0
- b1sl/b1sl/resources/_generated/occurrence_codes.py +15 -0
- b1sl/b1sl/resources/_generated/occurrence_codes_service.py +21 -0
- b1sl/b1sl/resources/_generated/orders.py +15 -0
- b1sl/b1sl/resources/_generated/orders_service.py +94 -0
- b1sl/b1sl/resources/_generated/packages_types.py +15 -0
- b1sl/b1sl/resources/_generated/partners_setups.py +15 -0
- b1sl/b1sl/resources/_generated/partners_setups_service.py +21 -0
- b1sl/b1sl/resources/_generated/payment_blocks.py +15 -0
- b1sl/b1sl/resources/_generated/payment_blocks_service.py +21 -0
- b1sl/b1sl/resources/_generated/payment_calculation_service.py +37 -0
- b1sl/b1sl/resources/_generated/payment_drafts.py +15 -0
- b1sl/b1sl/resources/_generated/payment_reason_code_service.py +21 -0
- b1sl/b1sl/resources/_generated/payment_reason_codes.py +15 -0
- b1sl/b1sl/resources/_generated/payment_run_export.py +15 -0
- b1sl/b1sl/resources/_generated/payment_terms_types.py +15 -0
- b1sl/b1sl/resources/_generated/payment_terms_types_service.py +28 -0
- b1sl/b1sl/resources/_generated/pick_lists.py +15 -0
- b1sl/b1sl/resources/_generated/pick_lists_service.py +78 -0
- b1sl/b1sl/resources/_generated/pos_daily_summary.py +15 -0
- b1sl/b1sl/resources/_generated/predefined_texts.py +15 -0
- b1sl/b1sl/resources/_generated/predefined_texts_service.py +21 -0
- b1sl/b1sl/resources/_generated/price_lists.py +15 -0
- b1sl/b1sl/resources/_generated/product_trees.py +15 -0
- b1sl/b1sl/resources/_generated/production_orders.py +15 -0
- b1sl/b1sl/resources/_generated/profit_centers.py +15 -0
- b1sl/b1sl/resources/_generated/profit_centers_service.py +21 -0
- b1sl/b1sl/resources/_generated/project_management_configuration_service.py +333 -0
- b1sl/b1sl/resources/_generated/project_management_service.py +104 -0
- b1sl/b1sl/resources/_generated/project_management_time_sheet.py +15 -0
- b1sl/b1sl/resources/_generated/project_managements.py +15 -0
- b1sl/b1sl/resources/_generated/projects.py +15 -0
- b1sl/b1sl/resources/_generated/projects_service.py +21 -0
- b1sl/b1sl/resources/_generated/purchase_credit_notes.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_credit_notes_service.py +85 -0
- b1sl/b1sl/resources/_generated/purchase_delivery_notes.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_delivery_notes_service.py +85 -0
- b1sl/b1sl/resources/_generated/purchase_down_payments.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_down_payments_service.py +70 -0
- b1sl/b1sl/resources/_generated/purchase_invoices.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_invoices_service.py +85 -0
- b1sl/b1sl/resources/_generated/purchase_orders.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_orders_service.py +69 -0
- b1sl/b1sl/resources/_generated/purchase_quotations.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_quotations_service.py +70 -0
- b1sl/b1sl/resources/_generated/purchase_request_service.py +68 -0
- b1sl/b1sl/resources/_generated/purchase_requests.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_returns.py +15 -0
- b1sl/b1sl/resources/_generated/purchase_returns_service.py +85 -0
- b1sl/b1sl/resources/_generated/purchase_tax_invoices.py +15 -0
- b1sl/b1sl/resources/_generated/qr_code_service.py +33 -0
- b1sl/b1sl/resources/_generated/query_auth_groups.py +15 -0
- b1sl/b1sl/resources/_generated/query_categories.py +15 -0
- b1sl/b1sl/resources/_generated/query_service.py +29 -0
- b1sl/b1sl/resources/_generated/queue.py +15 -0
- b1sl/b1sl/resources/_generated/quotations.py +15 -0
- b1sl/b1sl/resources/_generated/quotations_service.py +69 -0
- b1sl/b1sl/resources/_generated/recurring_transaction_service.py +67 -0
- b1sl/b1sl/resources/_generated/relationships.py +15 -0
- b1sl/b1sl/resources/_generated/report_filter.py +15 -0
- b1sl/b1sl/resources/_generated/report_filter_service.py +30 -0
- b1sl/b1sl/resources/_generated/report_layouts_service.py +237 -0
- b1sl/b1sl/resources/_generated/report_types.py +15 -0
- b1sl/b1sl/resources/_generated/report_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/resource_capacities.py +15 -0
- b1sl/b1sl/resources/_generated/resource_capacities_service.py +34 -0
- b1sl/b1sl/resources/_generated/resource_groups.py +15 -0
- b1sl/b1sl/resources/_generated/resource_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/resource_properties.py +15 -0
- b1sl/b1sl/resources/_generated/resource_properties_service.py +21 -0
- b1sl/b1sl/resources/_generated/resources.py +15 -0
- b1sl/b1sl/resources/_generated/resources_service.py +21 -0
- b1sl/b1sl/resources/_generated/retorno_codes.py +15 -0
- b1sl/b1sl/resources/_generated/retorno_codes_service.py +21 -0
- b1sl/b1sl/resources/_generated/return_request.py +15 -0
- b1sl/b1sl/resources/_generated/return_request_service.py +69 -0
- b1sl/b1sl/resources/_generated/returns.py +15 -0
- b1sl/b1sl/resources/_generated/returns_service.py +85 -0
- b1sl/b1sl/resources/_generated/route_stages.py +15 -0
- b1sl/b1sl/resources/_generated/route_stages_service.py +21 -0
- b1sl/b1sl/resources/_generated/sales_forecast.py +15 -0
- b1sl/b1sl/resources/_generated/sales_opportunities.py +15 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_competitors_setup.py +15 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_competitors_setup_service.py +21 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_interests_setup.py +15 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_interests_setup_service.py +21 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_reasons_setup.py +15 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_reasons_setup_service.py +21 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_sources_setup.py +15 -0
- b1sl/b1sl/resources/_generated/sales_opportunity_sources_setup_service.py +21 -0
- b1sl/b1sl/resources/_generated/sales_persons.py +15 -0
- b1sl/b1sl/resources/_generated/sales_stages.py +15 -0
- b1sl/b1sl/resources/_generated/sales_tax_authorities.py +15 -0
- b1sl/b1sl/resources/_generated/sales_tax_authorities_types.py +15 -0
- b1sl/b1sl/resources/_generated/sales_tax_codes.py +15 -0
- b1sl/b1sl/resources/_generated/sales_tax_invoices.py +15 -0
- b1sl/b1sl/resources/_generated/sbo_bob_service.py +89 -0
- b1sl/b1sl/resources/_generated/sections.py +15 -0
- b1sl/b1sl/resources/_generated/sections_service.py +21 -0
- b1sl/b1sl/resources/_generated/self_credit_memo_service.py +69 -0
- b1sl/b1sl/resources/_generated/self_credit_memos.py +15 -0
- b1sl/b1sl/resources/_generated/self_invoice_service.py +69 -0
- b1sl/b1sl/resources/_generated/self_invoices.py +15 -0
- b1sl/b1sl/resources/_generated/serial_number_details.py +15 -0
- b1sl/b1sl/resources/_generated/series_service.py +305 -0
- b1sl/b1sl/resources/_generated/service_call_origins.py +15 -0
- b1sl/b1sl/resources/_generated/service_call_origins_service.py +21 -0
- b1sl/b1sl/resources/_generated/service_call_problem_sub_types.py +15 -0
- b1sl/b1sl/resources/_generated/service_call_problem_sub_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/service_call_problem_types.py +15 -0
- b1sl/b1sl/resources/_generated/service_call_problem_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/service_call_solution_status.py +15 -0
- b1sl/b1sl/resources/_generated/service_call_solution_status_service.py +21 -0
- b1sl/b1sl/resources/_generated/service_call_status.py +15 -0
- b1sl/b1sl/resources/_generated/service_call_status_service.py +21 -0
- b1sl/b1sl/resources/_generated/service_call_types.py +15 -0
- b1sl/b1sl/resources/_generated/service_call_types_service.py +21 -0
- b1sl/b1sl/resources/_generated/service_calls.py +15 -0
- b1sl/b1sl/resources/_generated/service_contracts.py +15 -0
- b1sl/b1sl/resources/_generated/service_groups.py +15 -0
- b1sl/b1sl/resources/_generated/service_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/service_tax_posting_service.py +36 -0
- b1sl/b1sl/resources/_generated/shipping_types.py +15 -0
- b1sl/b1sl/resources/_generated/short_link_mappings_service.py +26 -0
- b1sl/b1sl/resources/_generated/special_prices.py +15 -0
- b1sl/b1sl/resources/_generated/states.py +15 -0
- b1sl/b1sl/resources/_generated/states_service.py +21 -0
- b1sl/b1sl/resources/_generated/stock_takings.py +15 -0
- b1sl/b1sl/resources/_generated/stock_transfer_draft_service.py +34 -0
- b1sl/b1sl/resources/_generated/stock_transfer_drafts.py +15 -0
- b1sl/b1sl/resources/_generated/stock_transfer_service.py +43 -0
- b1sl/b1sl/resources/_generated/stock_transfers.py +15 -0
- b1sl/b1sl/resources/_generated/target_groups.py +15 -0
- b1sl/b1sl/resources/_generated/target_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/tax_code_determinations.py +15 -0
- b1sl/b1sl/resources/_generated/tax_code_determinations_service.py +21 -0
- b1sl/b1sl/resources/_generated/tax_code_determinations_tcd.py +15 -0
- b1sl/b1sl/resources/_generated/tax_code_determinations_tcd_service.py +21 -0
- b1sl/b1sl/resources/_generated/tax_invoice_report.py +15 -0
- b1sl/b1sl/resources/_generated/tax_web_sites.py +15 -0
- b1sl/b1sl/resources/_generated/tax_web_sites_service.py +27 -0
- b1sl/b1sl/resources/_generated/teams.py +15 -0
- b1sl/b1sl/resources/_generated/termination_reason.py +15 -0
- b1sl/b1sl/resources/_generated/termination_reason_service.py +21 -0
- b1sl/b1sl/resources/_generated/territories.py +15 -0
- b1sl/b1sl/resources/_generated/tracking_notes.py +15 -0
- b1sl/b1sl/resources/_generated/tracking_notes_service.py +21 -0
- b1sl/b1sl/resources/_generated/transaction_codes.py +15 -0
- b1sl/b1sl/resources/_generated/transaction_codes_service.py +21 -0
- b1sl/b1sl/resources/_generated/transportation_document.py +15 -0
- b1sl/b1sl/resources/_generated/tsr_exceptional_event_service.py +21 -0
- b1sl/b1sl/resources/_generated/tsr_exceptional_events.py +15 -0
- b1sl/b1sl/resources/_generated/unit_of_measurement_groups.py +15 -0
- b1sl/b1sl/resources/_generated/unit_of_measurement_groups_service.py +21 -0
- b1sl/b1sl/resources/_generated/unit_of_measurements.py +15 -0
- b1sl/b1sl/resources/_generated/unit_of_measurements_service.py +21 -0
- b1sl/b1sl/resources/_generated/user_default_groups.py +15 -0
- b1sl/b1sl/resources/_generated/user_fields_md.py +15 -0
- b1sl/b1sl/resources/_generated/user_keys_md.py +15 -0
- b1sl/b1sl/resources/_generated/user_languages.py +15 -0
- b1sl/b1sl/resources/_generated/user_menu_service.py +103 -0
- b1sl/b1sl/resources/_generated/user_objects_md.py +15 -0
- b1sl/b1sl/resources/_generated/user_permission_tree.py +15 -0
- b1sl/b1sl/resources/_generated/user_queries.py +15 -0
- b1sl/b1sl/resources/_generated/user_tables_md.py +15 -0
- b1sl/b1sl/resources/_generated/users.py +15 -0
- b1sl/b1sl/resources/_generated/users_service.py +21 -0
- b1sl/b1sl/resources/_generated/value_mapping.py +15 -0
- b1sl/b1sl/resources/_generated/value_mapping_communication.py +15 -0
- b1sl/b1sl/resources/_generated/value_mapping_service.py +64 -0
- b1sl/b1sl/resources/_generated/vat_groups.py +15 -0
- b1sl/b1sl/resources/_generated/vendor_payments.py +15 -0
- b1sl/b1sl/resources/_generated/w_tax_type_code_service.py +21 -0
- b1sl/b1sl/resources/_generated/w_tax_type_codes.py +15 -0
- b1sl/b1sl/resources/_generated/warehouse_locations.py +15 -0
- b1sl/b1sl/resources/_generated/warehouse_sublevel_codes.py +15 -0
- b1sl/b1sl/resources/_generated/warehouse_sublevel_codes_service.py +21 -0
- b1sl/b1sl/resources/_generated/warehouses.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_bookmark_tile_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_bookmark_tiles.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_dashboard_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_dashboards.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_form_setting_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_form_settings.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_launchpad_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_launchpads.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_listview_filter_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_listview_filters.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_notification_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_notifications.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_preference_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_preferences.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_recent_activities.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_recent_activity_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_variant_group_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_variant_groups.py +15 -0
- b1sl/b1sl/resources/_generated/web_client_variant_service.py +21 -0
- b1sl/b1sl/resources/_generated/web_client_variants.py +15 -0
- b1sl/b1sl/resources/_generated/weight_measures.py +15 -0
- b1sl/b1sl/resources/_generated/withholding_tax_codes.py +15 -0
- b1sl/b1sl/resources/_generated/witholding_tax_definition.py +15 -0
- b1sl/b1sl/resources/_generated/wizard_payment_methods.py +15 -0
- b1sl/b1sl/resources/_generated/workflow_task_service.py +47 -0
- b1sl/b1sl/resources/async_base.py +60 -0
- b1sl/b1sl/resources/base.py +159 -0
- b1sl/b1sl/resources/udo.py +78 -0
- b1sl/b1sl/rest_adapter.py +351 -0
- b1sl/b1sl/testing.py +24 -0
- b1sl/b1sl/tests/__init__.py +0 -0
- b1sl/b1sl/tests/test_01_settings.py +60 -0
- b1sl/b1sl/tests/test_02_rest_adapter.py +177 -0
- b1sl/b1sl/tests/test_03_items_endpoint.py +172 -0
- b1sl/b1sl/tests/test_04_serialnumberdetails_endpoint.py +181 -0
- b1sl/b1sl/tests/test_observability.py +153 -0
- b1sl/b1sl/tests/test_utils.py +24 -0
- b1sl/contrib/__init__.py +0 -0
- b1sl/contrib/django/__init__.py +15 -0
- b1sl/contrib/django/base.py +89 -0
- b1sl/contrib/django/odata_decode_middleware.py +14 -0
- b1sl/contrib/django/odata_transform_url_middleware.py +15 -0
- b1sl/py.typed +0 -0
- b1sl/saphdb/__init__.py +19 -0
- b1sl/saphdb/adapter.py +24 -0
- b1sl/saphdb/client.py +50 -0
- b1sl/saphdb/config.py +68 -0
- b1sl/saphdb/endpoints/serialnumberdetailodbc.py +100 -0
- b1sl/saphdb/exceptions/exceptions.py +19 -0
- b1sl/saphdb/main.py +18 -0
- b1sl/saphdb/models/result.py +18 -0
- b1sl/saphdb/models/serial.py +37 -0
- b1sl/saphdb/odbc_adapter.py +220 -0
- b1sl/saphdb/tests/__init__.py +0 -0
- b1sl/saphdb/tests/test_01_settings.py +37 -0
- b1sl/saphdb/tests/test_02_odbc_adapter.py +96 -0
- b1sl/saphdb/tests/test_04_serialnumberdetails_endpoint.py +131 -0
- b1sl_python-0.1.0.dist-info/METADATA +169 -0
- b1sl_python-0.1.0.dist-info/RECORD +579 -0
- b1sl_python-0.1.0.dist-info/WHEEL +4 -0
- b1sl_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from json import JSONDecodeError
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from urllib.parse import urlencode, urlsplit
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from b1sl.b1sl.base_adapter import BaseRestAdapter, HookContext, ObservabilityConfig
|
|
15
|
+
from b1sl.b1sl.config import B1Config
|
|
16
|
+
from b1sl.b1sl.exceptions.exceptions import (
|
|
17
|
+
B1AuthError,
|
|
18
|
+
B1Exception,
|
|
19
|
+
B1NotFoundError,
|
|
20
|
+
B1ValidationError,
|
|
21
|
+
SAPConcurrencyError,
|
|
22
|
+
)
|
|
23
|
+
from b1sl.b1sl.models.result import Result
|
|
24
|
+
|
|
25
|
+
_HTTP_STATUS_TO_EXC: dict[int, type] = {
|
|
26
|
+
400: B1ValidationError,
|
|
27
|
+
401: B1AuthError,
|
|
28
|
+
404: B1NotFoundError,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RestAdapter(BaseRestAdapter):
|
|
33
|
+
"""
|
|
34
|
+
Synchronous HTTP adapter for SAP B1 Service Layer using requests.
|
|
35
|
+
|
|
36
|
+
This adapter manages session lifecycle (login/logout), re-authentication
|
|
37
|
+
on 401 errors, and ETag-based optimistic concurrency for single-threaded
|
|
38
|
+
or WSGI environments. It uses httpx.Client for parity with the async client.
|
|
39
|
+
|
|
40
|
+
AI Role: Use for synchronous workflows or legacy integrations.
|
|
41
|
+
Handles basic CRUD operations and session resilience automatically.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __init__(self, *args, **kwargs):
|
|
47
|
+
"""
|
|
48
|
+
Initializes the synchronous httpx client.
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(*args, **kwargs)
|
|
51
|
+
self._lock = threading.Lock()
|
|
52
|
+
self.session = httpx.Client(
|
|
53
|
+
verify=self._ssl_verify,
|
|
54
|
+
timeout=httpx.Timeout(
|
|
55
|
+
self._connect_timeout,
|
|
56
|
+
read=self._read_timeout,
|
|
57
|
+
write=self._read_timeout,
|
|
58
|
+
pool=self._connect_timeout,
|
|
59
|
+
),
|
|
60
|
+
follow_redirects=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def close(self) -> None:
|
|
64
|
+
"""Closes the underlying HTTP client."""
|
|
65
|
+
if self.session:
|
|
66
|
+
self.session.close()
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def session_id(self) -> str | None:
|
|
70
|
+
"""
|
|
71
|
+
Retrieves the current SAP session ID from the cookie jar.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: The B1SESSION cookie value or None.
|
|
75
|
+
"""
|
|
76
|
+
return self.session.cookies.get("B1SESSION")
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def handle_token(manage_token: bool = True):
|
|
80
|
+
"""
|
|
81
|
+
Decorator for methods requiring a valid SAP session.
|
|
82
|
+
|
|
83
|
+
Logs in before the method call if needed and potentially logs out
|
|
84
|
+
afterwards depending on the reuse_token configuration.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
manage_token (bool): Whether to perform session lifecycle management.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def decorator(func):
|
|
91
|
+
@functools.wraps(func)
|
|
92
|
+
def wrapper(self, *args, **kwargs):
|
|
93
|
+
if manage_token:
|
|
94
|
+
self._handle_token_login()
|
|
95
|
+
try:
|
|
96
|
+
result = func(self, *args, **kwargs)
|
|
97
|
+
finally:
|
|
98
|
+
if manage_token:
|
|
99
|
+
self._handle_token_logout()
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
return wrapper
|
|
103
|
+
|
|
104
|
+
return decorator
|
|
105
|
+
|
|
106
|
+
def login(self):
|
|
107
|
+
"""Authenticate with the SAP B1 Service Layer."""
|
|
108
|
+
return self._login()
|
|
109
|
+
|
|
110
|
+
def logout(self):
|
|
111
|
+
"""Release the SAP B1 session license."""
|
|
112
|
+
return self._logout()
|
|
113
|
+
|
|
114
|
+
def _login(self):
|
|
115
|
+
"""
|
|
116
|
+
Internal login implementation.
|
|
117
|
+
|
|
118
|
+
Calls POST /Login and updates session state and expiry.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Result: Response from the Login endpoint.
|
|
122
|
+
"""
|
|
123
|
+
data = {
|
|
124
|
+
"UserName": self._username,
|
|
125
|
+
"Password": self._password,
|
|
126
|
+
"CompanyDB": self._db,
|
|
127
|
+
}
|
|
128
|
+
self._logger.info(f"Logging in to SAP B1 at {self.raw_base_url}...")
|
|
129
|
+
|
|
130
|
+
response = self._do(
|
|
131
|
+
http_method="POST", endpoint="/Login", data=data, _is_login=True
|
|
132
|
+
)
|
|
133
|
+
if response.status_code == 200:
|
|
134
|
+
self.is_session_active = True
|
|
135
|
+
timeout_min = response.data.get("SessionTimeout", 30)
|
|
136
|
+
self.token_expiry = datetime.now() + timedelta(minutes=timeout_min - 2)
|
|
137
|
+
return response
|
|
138
|
+
|
|
139
|
+
def _logout(self):
|
|
140
|
+
"""
|
|
141
|
+
Internal logout implementation.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Result: Response from the Logout endpoint.
|
|
145
|
+
"""
|
|
146
|
+
response = self._do(http_method="POST", endpoint="/Logout", _is_login=True)
|
|
147
|
+
if response.status_code == 204:
|
|
148
|
+
self.is_session_active = False
|
|
149
|
+
self.token_expiry = None
|
|
150
|
+
return response
|
|
151
|
+
|
|
152
|
+
def _is_token_expire(self) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Checks if the current session token is missing or expired.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
bool: True if a new login is required.
|
|
158
|
+
"""
|
|
159
|
+
return self.token_expiry is None or datetime.now() >= self.token_expiry
|
|
160
|
+
|
|
161
|
+
def _handle_token_login(self):
|
|
162
|
+
"""Standard login handler used by the decorator."""
|
|
163
|
+
if self.reuse_token:
|
|
164
|
+
if self._is_token_expire():
|
|
165
|
+
with self._lock:
|
|
166
|
+
if self._is_token_expire():
|
|
167
|
+
self._login()
|
|
168
|
+
else:
|
|
169
|
+
with self._lock:
|
|
170
|
+
self._login()
|
|
171
|
+
|
|
172
|
+
def _handle_token_logout(self):
|
|
173
|
+
"""Standard logout handler used by the decorator."""
|
|
174
|
+
if not self.reuse_token:
|
|
175
|
+
self._logout()
|
|
176
|
+
|
|
177
|
+
def _get_full_url(self, next_link: str) -> str:
|
|
178
|
+
"""Normalizes a nextLink to a relative resource path."""
|
|
179
|
+
return "/" + urlsplit(next_link).path.lstrip("/")
|
|
180
|
+
|
|
181
|
+
def _process_next_link(self, next_link: str) -> tuple[str, str, dict]:
|
|
182
|
+
"""Parses an OData nextLink for multi-page results."""
|
|
183
|
+
return (
|
|
184
|
+
self._clean_url(next_link),
|
|
185
|
+
self._get_full_url(next_link),
|
|
186
|
+
self._get_ep_params(next_link),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _parse_sap_error(response: httpx.Response) -> tuple[str, str]:
|
|
191
|
+
"""Parses error information from an Httpx response object."""
|
|
192
|
+
try:
|
|
193
|
+
body = response.json()
|
|
194
|
+
except Exception:
|
|
195
|
+
body = None
|
|
196
|
+
return RestAdapter._parse_sap_error_shared(response.status_code, response.reason_phrase, body)
|
|
197
|
+
|
|
198
|
+
def _execute_request(
|
|
199
|
+
self,
|
|
200
|
+
http_method: str,
|
|
201
|
+
full_url: str,
|
|
202
|
+
headers: dict,
|
|
203
|
+
ep_params: dict | None,
|
|
204
|
+
data: dict | None,
|
|
205
|
+
) -> httpx.Response:
|
|
206
|
+
"""Executes the raw HTTP request using the httpx client."""
|
|
207
|
+
return self.session.request(
|
|
208
|
+
method=http_method,
|
|
209
|
+
url=full_url,
|
|
210
|
+
headers=headers,
|
|
211
|
+
params=ep_params,
|
|
212
|
+
json=data,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _do(
|
|
216
|
+
self,
|
|
217
|
+
http_method: str,
|
|
218
|
+
endpoint: str,
|
|
219
|
+
ep_params: dict | None = None,
|
|
220
|
+
data: dict | None = None,
|
|
221
|
+
_is_login: bool = False,
|
|
222
|
+
) -> Result:
|
|
223
|
+
"""
|
|
224
|
+
Dispatches a standardized HTTP request to SAP.
|
|
225
|
+
Implements Senior Observability (Timing + Structured Logging + Hooks).
|
|
226
|
+
"""
|
|
227
|
+
# Removed redundant imports moved to module level
|
|
228
|
+
req_id = self._generate_req_id()
|
|
229
|
+
endpoint_path = endpoint if endpoint.startswith("/") else f"/{endpoint}"
|
|
230
|
+
full_url = self.raw_base_url + endpoint_path
|
|
231
|
+
|
|
232
|
+
# ── ETag: inject If-None-Match (GET) or If-Match (PATCH/DELETE/POST) ──
|
|
233
|
+
headers = self._build_headers(http_method, endpoint_path)
|
|
234
|
+
|
|
235
|
+
log_data = self._redact_data(data)
|
|
236
|
+
self._logger.debug(f"[{req_id}] data={log_data}")
|
|
237
|
+
|
|
238
|
+
start_time = time.perf_counter()
|
|
239
|
+
exc_captured: Exception | None = None
|
|
240
|
+
response: httpx.Response | None = None
|
|
241
|
+
is_success = False
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
response = self._execute_request(
|
|
245
|
+
http_method, full_url, headers, ep_params, data
|
|
246
|
+
)
|
|
247
|
+
response.raise_for_status()
|
|
248
|
+
is_success = True
|
|
249
|
+
except httpx.HTTPStatusError as e:
|
|
250
|
+
if response is not None and response.status_code == 401 and not _is_login:
|
|
251
|
+
self._logger.warning(f"[{req_id}] 401 Unauthorized - retrying login...")
|
|
252
|
+
try:
|
|
253
|
+
old_token_expiry = self.token_expiry
|
|
254
|
+
with self._lock:
|
|
255
|
+
if self.token_expiry == old_token_expiry:
|
|
256
|
+
self._login()
|
|
257
|
+
response = self._execute_request(
|
|
258
|
+
http_method, full_url, headers, ep_params, data
|
|
259
|
+
)
|
|
260
|
+
response.raise_for_status()
|
|
261
|
+
is_success = True
|
|
262
|
+
except Exception as retry_exc:
|
|
263
|
+
exc_captured = retry_exc
|
|
264
|
+
raise B1Exception(
|
|
265
|
+
"Request failed after session retry"
|
|
266
|
+
) from retry_exc
|
|
267
|
+
else:
|
|
268
|
+
exc_captured = e
|
|
269
|
+
sap_code, sap_msg = self._parse_sap_error(response)
|
|
270
|
+
# ── Raise specialised exception before falling back to B1Exception ──
|
|
271
|
+
try:
|
|
272
|
+
body = response.json() if response.content else None
|
|
273
|
+
except Exception:
|
|
274
|
+
body = None
|
|
275
|
+
self._raise_if_concurrency_error(
|
|
276
|
+
response.status_code, sap_code, sap_msg, endpoint_path, body
|
|
277
|
+
)
|
|
278
|
+
raise B1Exception(f"SAP Error {sap_code}: {sap_msg}") from e
|
|
279
|
+
except Exception as e:
|
|
280
|
+
exc_captured = e
|
|
281
|
+
raise B1Exception(f"Request failed: {e}") from e
|
|
282
|
+
finally:
|
|
283
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
284
|
+
status_code = response.status_code if response is not None else None
|
|
285
|
+
|
|
286
|
+
ctx = HookContext(
|
|
287
|
+
req_id=req_id,
|
|
288
|
+
http_method=http_method,
|
|
289
|
+
base_url=self.raw_base_url,
|
|
290
|
+
endpoint=endpoint_path,
|
|
291
|
+
query_params=urlencode(ep_params) if ep_params else "",
|
|
292
|
+
db=self._db,
|
|
293
|
+
user=self._username,
|
|
294
|
+
status_code=status_code,
|
|
295
|
+
duration_ms=duration_ms,
|
|
296
|
+
extra=dict(self._obs.context_extras),
|
|
297
|
+
exc=exc_captured,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
self._log_response(ctx)
|
|
301
|
+
self._hooks.dispatch(
|
|
302
|
+
"on_error" if exc_captured else "on_response", ctx, self._logger
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if is_success and response is not None:
|
|
306
|
+
if response.content:
|
|
307
|
+
try:
|
|
308
|
+
data_out = response.json()
|
|
309
|
+
# ── ETag: extract from header (preferred) or body fallback ──
|
|
310
|
+
self._extract_etag(endpoint_path, dict(response.headers), data_out)
|
|
311
|
+
return Result(
|
|
312
|
+
status_code=response.status_code,
|
|
313
|
+
message=response.reason_phrase,
|
|
314
|
+
data=data_out,
|
|
315
|
+
next_link=data_out.get("odata.nextLink"),
|
|
316
|
+
next_params=self._get_ep_params(data_out.get("odata.nextLink"))
|
|
317
|
+
if data_out.get("odata.nextLink")
|
|
318
|
+
else None,
|
|
319
|
+
metadata=data_out.get("odata.metadata"),
|
|
320
|
+
)
|
|
321
|
+
except Exception:
|
|
322
|
+
raise B1Exception("Bad JSON response")
|
|
323
|
+
else:
|
|
324
|
+
self._extract_etag(endpoint_path, dict(response.headers), None)
|
|
325
|
+
return Result(
|
|
326
|
+
status_code=response.status_code, message=response.reason_phrase, data=None
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
raise B1Exception(
|
|
330
|
+
f"HTTP Error {response.status_code if response else 'Unknown'}"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
@handle_token()
|
|
334
|
+
def get(self, endpoint, ep_params=None, data=None):
|
|
335
|
+
"""Execute a synchronous GET request."""
|
|
336
|
+
return self._do("GET", endpoint, ep_params, data)
|
|
337
|
+
|
|
338
|
+
@handle_token()
|
|
339
|
+
def post(self, endpoint, ep_params=None, data=None):
|
|
340
|
+
"""Execute a synchronous POST request."""
|
|
341
|
+
return self._do("POST", endpoint, ep_params, data)
|
|
342
|
+
|
|
343
|
+
@handle_token()
|
|
344
|
+
def patch(self, endpoint, ep_params=None, data=None):
|
|
345
|
+
"""Execute a synchronous PATCH request."""
|
|
346
|
+
return self._do("PATCH", endpoint, ep_params, data)
|
|
347
|
+
|
|
348
|
+
@handle_token()
|
|
349
|
+
def delete(self, endpoint, ep_params=None, data=None):
|
|
350
|
+
"""Execute a synchronous DELETE request."""
|
|
351
|
+
return self._do("DELETE", endpoint, ep_params, data)
|
b1sl/b1sl/testing.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class B1TestHelper:
|
|
7
|
+
"""
|
|
8
|
+
Test helper for SAP B1 environments.
|
|
9
|
+
Groups test_data logic away from the core runtime SDK.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, test_data: dict[str, Any]):
|
|
12
|
+
self.test_data = test_data
|
|
13
|
+
|
|
14
|
+
def get_test_item(self, profile: str = "simple") -> str:
|
|
15
|
+
"""Retrieves a test item code from the environment test data."""
|
|
16
|
+
return self.test_data.get("items", {}).get(profile, "")
|
|
17
|
+
|
|
18
|
+
def get_test_customer(self, profile: str = "retail") -> str:
|
|
19
|
+
"""Retrieves a test customer code from the environment test data."""
|
|
20
|
+
return self.test_data.get("customers", {}).get(profile, "")
|
|
21
|
+
|
|
22
|
+
def get_test_bp(self, profile: str = "simple") -> str:
|
|
23
|
+
"""Retrieves a test Business Partner code from the environment test data."""
|
|
24
|
+
return self.test_data.get("business_partners", {}).get(profile, "")
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
test_01_config.py — B1Config unit tests (pure pytest, no Django).
|
|
3
|
+
|
|
4
|
+
Renamed from test_01_settings.py which required Django settings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from b1sl.b1sl.config import B1Config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_config_rejects_empty_base_url():
|
|
15
|
+
with pytest.raises(ValueError, match="base_url"):
|
|
16
|
+
B1Config(base_url="", username="u", password="p", company_db="db")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_config_rejects_empty_credentials():
|
|
20
|
+
with pytest.raises(ValueError, match="credentials"):
|
|
21
|
+
B1Config(base_url="https://x", username="", password="", company_db="db")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_config_rejects_empty_company_db():
|
|
25
|
+
with pytest.raises(ValueError, match="company_db"):
|
|
26
|
+
B1Config(base_url="https://x", username="u", password="p", company_db="")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_config_defaults():
|
|
30
|
+
cfg = B1Config(base_url="https://x", username="u", password="p", company_db="db")
|
|
31
|
+
assert cfg.ssl_verify is True
|
|
32
|
+
assert cfg.reuse_token is True
|
|
33
|
+
assert cfg.token_timeout == timedelta(seconds=900)
|
|
34
|
+
assert cfg.max_page_size == 20
|
|
35
|
+
assert cfg.connect_timeout == 10.0
|
|
36
|
+
assert cfg.read_timeout == 60.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_config_from_env_success():
|
|
40
|
+
with pytest.MonkeyPatch.context() as mp:
|
|
41
|
+
mp.setenv("SAPB1CLIENT_BASE_URL", "https://sap:50000/b1s/v1")
|
|
42
|
+
mp.setenv("SAPB1CLIENT_USERNAME", "admin")
|
|
43
|
+
mp.setenv("SAPB1CLIENT_PASSWORD", "pass")
|
|
44
|
+
mp.setenv("SAPB1CLIENT_COMPANY_DB", "MYDB")
|
|
45
|
+
cfg = B1Config.from_env()
|
|
46
|
+
assert cfg.base_url == "https://sap:50000/b1s/v1"
|
|
47
|
+
assert cfg.company_db == "MYDB"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_config_from_env_missing_raises():
|
|
51
|
+
with pytest.MonkeyPatch.context() as mp:
|
|
52
|
+
for key in (
|
|
53
|
+
"SAPB1CLIENT_BASE_URL",
|
|
54
|
+
"SAPB1CLIENT_USERNAME",
|
|
55
|
+
"SAPB1CLIENT_PASSWORD",
|
|
56
|
+
"SAPB1CLIENT_COMPANY_DB",
|
|
57
|
+
):
|
|
58
|
+
mp.delenv(key, raising=False)
|
|
59
|
+
with pytest.raises(EnvironmentError):
|
|
60
|
+
B1Config.from_env()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
test_02_rest_adapter.py — RestAdapter unit tests (pure pytest, no Django).
|
|
3
|
+
|
|
4
|
+
These are offline tests using mocks — no live SAP connection required.
|
|
5
|
+
Integration tests that require a live SAP instance belong in a separate
|
|
6
|
+
``tests/integration/`` directory that is skipped in CI.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import timedelta
|
|
10
|
+
from json import JSONDecodeError
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from b1sl.b1sl.config import B1Config
|
|
16
|
+
from b1sl.b1sl.rest_adapter import RestAdapter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def config():
|
|
21
|
+
return B1Config(
|
|
22
|
+
base_url="https://sap:50000/b1s/v1",
|
|
23
|
+
username="manager",
|
|
24
|
+
password="s3cr3t",
|
|
25
|
+
company_db="TESTDB",
|
|
26
|
+
reuse_token=False,
|
|
27
|
+
connect_timeout=5.0,
|
|
28
|
+
read_timeout=30.0,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def adapter(config):
|
|
34
|
+
return RestAdapter.from_config(config)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Singleton removed ────────────────────────────────────────────────────── #
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_no_singleton(config):
|
|
41
|
+
"""Two from_config() calls must return two distinct objects."""
|
|
42
|
+
a = RestAdapter.from_config(config)
|
|
43
|
+
b = RestAdapter.from_config(config)
|
|
44
|
+
assert a is not b
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Constructor ──────────────────────────────────────────────────────────── #
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_adapter_stores_config_fields(adapter, config):
|
|
51
|
+
assert adapter.url == config.base_url
|
|
52
|
+
assert adapter._username == config.username
|
|
53
|
+
assert adapter._connect_timeout == 5.0
|
|
54
|
+
assert adapter._read_timeout == 30.0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Token expiry ─────────────────────────────────────────────────────────── #
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_is_token_expire_when_no_expiry(adapter):
|
|
61
|
+
adapter.token_expiry = None
|
|
62
|
+
assert adapter._is_token_expire() is True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_is_token_expire_when_future(adapter):
|
|
66
|
+
from datetime import datetime
|
|
67
|
+
|
|
68
|
+
adapter.token_expiry = datetime.now() + timedelta(hours=1)
|
|
69
|
+
assert adapter._is_token_expire() is False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_is_token_expire_when_past(adapter):
|
|
73
|
+
from datetime import datetime
|
|
74
|
+
|
|
75
|
+
adapter.token_expiry = datetime.now() - timedelta(seconds=1)
|
|
76
|
+
assert adapter._is_token_expire() is True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── _parse_sap_error ─────────────────────────────────────────────────────── #
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _mock_resp(status, body=None, json_raises=False):
|
|
83
|
+
r = MagicMock()
|
|
84
|
+
r.status_code = status
|
|
85
|
+
r.reason = "Error"
|
|
86
|
+
if json_raises:
|
|
87
|
+
r.json.side_effect = JSONDecodeError("", "", 0)
|
|
88
|
+
else:
|
|
89
|
+
r.json.return_value = body
|
|
90
|
+
return r
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize(
|
|
94
|
+
"body,exp_code,exp_fragment",
|
|
95
|
+
[
|
|
96
|
+
(
|
|
97
|
+
{"error": {"code": -2028, "message": {"value": "Item does not exist"}}},
|
|
98
|
+
"-2028",
|
|
99
|
+
"Item does not exist",
|
|
100
|
+
),
|
|
101
|
+
(
|
|
102
|
+
{"error": {"code": "-5002", "message": "No records found"}},
|
|
103
|
+
"-5002",
|
|
104
|
+
"No records found",
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
{"error": {"code": 0, "message": {"lang": "en", "value": "Auth error"}}},
|
|
108
|
+
"0",
|
|
109
|
+
"Auth error",
|
|
110
|
+
),
|
|
111
|
+
({"detail": "not SAP format"}, "unknown", "HTTP 400"),
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
def test_parse_sap_error_shapes(body, exp_code, exp_fragment):
|
|
115
|
+
r = _mock_resp(400, body)
|
|
116
|
+
code, msg = RestAdapter._parse_sap_error(r)
|
|
117
|
+
assert code == exp_code
|
|
118
|
+
assert exp_fragment in msg
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_parse_sap_error_non_json():
|
|
122
|
+
r = _mock_resp(500, json_raises=True)
|
|
123
|
+
r.reason = "Internal Server Error"
|
|
124
|
+
code, msg = RestAdapter._parse_sap_error(r)
|
|
125
|
+
assert code == "unknown"
|
|
126
|
+
assert "500" in msg
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ── Password never appears in debug logs ─────────────────────────────────── #
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_password_not_logged(adapter, caplog):
|
|
133
|
+
"""The Password field must be redacted before any log statement in _do."""
|
|
134
|
+
import logging
|
|
135
|
+
|
|
136
|
+
with caplog.at_level(logging.DEBUG, logger="b1sl.b1sl.rest_adapter"):
|
|
137
|
+
with patch.object(adapter, "_execute_request") as mock_exec:
|
|
138
|
+
mock_resp = MagicMock()
|
|
139
|
+
mock_resp.status_code = 200
|
|
140
|
+
mock_resp.reason = "OK" # must be str for Result.message
|
|
141
|
+
mock_resp.content = b'{"SessionId":"abc","Version":"1","SessionTimeout":30}'
|
|
142
|
+
mock_resp.json.return_value = {
|
|
143
|
+
"SessionId": "abc",
|
|
144
|
+
"Version": "1",
|
|
145
|
+
"SessionTimeout": 30,
|
|
146
|
+
}
|
|
147
|
+
mock_resp.headers = {}
|
|
148
|
+
mock_resp.raise_for_status.return_value = None
|
|
149
|
+
mock_exec.return_value = mock_resp
|
|
150
|
+
adapter._do(
|
|
151
|
+
http_method="POST",
|
|
152
|
+
endpoint="/Login",
|
|
153
|
+
data={
|
|
154
|
+
"UserName": "manager",
|
|
155
|
+
"Password": "s3cr3t",
|
|
156
|
+
"CompanyDB": "TESTDB",
|
|
157
|
+
},
|
|
158
|
+
_is_login=True,
|
|
159
|
+
)
|
|
160
|
+
full_log = " ".join(caplog.messages)
|
|
161
|
+
assert "s3cr3t" not in full_log, "Raw password found in log output!"
|
|
162
|
+
assert "***" in full_log, "Password redaction marker '***' missing from log"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── Timeout passed to session.request ────────────────────────────────────── #
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_execute_request_passes_timeout(adapter):
|
|
169
|
+
with patch.object(adapter.session, "request") as mock_req:
|
|
170
|
+
mock_resp = MagicMock()
|
|
171
|
+
mock_resp.status_code = 200
|
|
172
|
+
mock_resp.headers = {}
|
|
173
|
+
mock_req.return_value = mock_resp
|
|
174
|
+
adapter._execute_request("GET", "https://sap/b1s/v1/Items", {}, None, None)
|
|
175
|
+
_, kwargs = mock_req.call_args
|
|
176
|
+
assert "timeout" in kwargs
|
|
177
|
+
assert kwargs["timeout"] == (5.0, 30.0)
|