b1sl-python 0.1.2__py3-none-any.whl → 0.2.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/b1sl/__init__.py +6 -10
- b1sl/b1sl/async_client.py +126 -81
- b1sl/b1sl/async_rest_adapter.py +11 -2
- b1sl/b1sl/base_adapter.py +26 -1
- b1sl/b1sl/client.py +222 -16
- b1sl/b1sl/exceptions/__init__.py +19 -0
- b1sl/b1sl/models/_generated/complex_types.py +261 -261
- b1sl/b1sl/models/_generated/entities/businesspartners.py +30 -30
- b1sl/b1sl/models/_generated/entities/finance.py +24 -24
- b1sl/b1sl/models/_generated/entities/general.py +225 -225
- b1sl/b1sl/models/_generated/entities/inventory.py +43 -43
- b1sl/b1sl/models/_generated/entities/production.py +19 -19
- b1sl/b1sl/models/_generated/entities/purchasing.py +3 -3
- b1sl/b1sl/models/_generated/entities/sales.py +16 -16
- b1sl/b1sl/models/base.py +11 -1
- b1sl/b1sl/resources/async_base.py +94 -4
- b1sl/b1sl/resources/base.py +34 -8
- b1sl/b1sl/resources/odata.py +84 -2
- b1sl/b1sl/rest_adapter.py +11 -2
- {b1sl_python-0.1.2.dist-info → b1sl_python-0.2.0.dist-info}/METADATA +2 -2
- {b1sl_python-0.1.2.dist-info → b1sl_python-0.2.0.dist-info}/RECORD +23 -23
- b1sl/b1sl/resources/_generated/client_mixin.py +0 -3920
- {b1sl_python-0.1.2.dist-info → b1sl_python-0.2.0.dist-info}/WHEEL +0 -0
- {b1sl_python-0.1.2.dist-info → b1sl_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
b1sl/b1sl/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ b1sl.b1sl — SDK for SAP B1 Service Layer (OData).
|
|
|
5
5
|
import logging
|
|
6
6
|
import warnings
|
|
7
7
|
|
|
8
|
+
from b1sl.b1sl import entities
|
|
8
9
|
from b1sl.b1sl.async_client import AsyncB1Client
|
|
9
10
|
from b1sl.b1sl.async_rest_adapter import AsyncRestAdapter
|
|
10
11
|
from b1sl.b1sl.base_adapter import HookContext, ObservabilityConfig
|
|
@@ -17,6 +18,11 @@ from b1sl.b1sl.resources.async_base import AsyncGenericResource
|
|
|
17
18
|
from b1sl.b1sl.resources.base import GenericResource, ODataQuery
|
|
18
19
|
from b1sl.b1sl.rest_adapter import RestAdapter
|
|
19
20
|
|
|
21
|
+
try:
|
|
22
|
+
from b1sl.b1sl import fields # type: ignore
|
|
23
|
+
except ImportError:
|
|
24
|
+
fields = None # type: ignore
|
|
25
|
+
|
|
20
26
|
try:
|
|
21
27
|
from pydantic import ArbitraryTypeWarning
|
|
22
28
|
|
|
@@ -27,16 +33,6 @@ except ImportError:
|
|
|
27
33
|
# Standard library pattern: prevent "No handlers could be found"
|
|
28
34
|
logging.getLogger("b1sl").addHandler(logging.NullHandler())
|
|
29
35
|
|
|
30
|
-
try:
|
|
31
|
-
from b1sl.b1sl import entities # type: ignore
|
|
32
|
-
except ImportError:
|
|
33
|
-
entities = None # type: ignore # Before code generation
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
from b1sl.b1sl import fields # type: ignore
|
|
37
|
-
except ImportError:
|
|
38
|
-
fields = None # type: ignore
|
|
39
|
-
|
|
40
36
|
__all__ = [
|
|
41
37
|
"AsyncB1Client",
|
|
42
38
|
"B1Client",
|
b1sl/b1sl/async_client.py
CHANGED
|
@@ -12,13 +12,8 @@ if TYPE_CHECKING:
|
|
|
12
12
|
Activity,
|
|
13
13
|
BusinessPartner,
|
|
14
14
|
)
|
|
15
|
-
from b1sl.b1sl.models._generated.entities.
|
|
16
|
-
from b1sl.b1sl.models._generated.entities.general import Document, Payment, User
|
|
17
|
-
|
|
18
|
-
# Models for typing convenience aliases
|
|
15
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
19
16
|
from b1sl.b1sl.models._generated.entities.inventory import Item
|
|
20
|
-
from b1sl.b1sl.models._generated.entities.production import ProductionOrder
|
|
21
|
-
from b1sl.b1sl.models._generated.entities.sales import ServiceCall
|
|
22
17
|
from b1sl.b1sl.models.base import B1Model
|
|
23
18
|
from b1sl.b1sl.resources.async_base import AsyncGenericResource
|
|
24
19
|
from b1sl.b1sl.resources.udo import AsyncUDOResource
|
|
@@ -35,6 +30,12 @@ class AsyncB1Client:
|
|
|
35
30
|
AI Role: Recommended for modern web apps.
|
|
36
31
|
Use 'async with AsyncB1Client(config) as b1:' to ensure session cleanup.
|
|
37
32
|
|
|
33
|
+
Concurrency-Elite Aliases (Elite Citizens):
|
|
34
|
+
Only entities with ETag support are exposed as direct properties.
|
|
35
|
+
This ensures state-safety and clear architectural boundaries.
|
|
36
|
+
Objects without ETag support must be accessed via 'get_resource()'
|
|
37
|
+
or 'udo()'.
|
|
38
|
+
|
|
38
39
|
Example:
|
|
39
40
|
async with AsyncB1Client(config) as b1:
|
|
40
41
|
item = await b1.items.get("A0001")
|
|
@@ -59,9 +60,10 @@ class AsyncB1Client:
|
|
|
59
60
|
version (str): API version (defaults to 'v2').
|
|
60
61
|
session_id (str, optional): An existing B1SESSION cookie to reuse.
|
|
61
62
|
"""
|
|
63
|
+
self._logger = logger or logging.getLogger(f"b1sl.{self.__class__.__name__}")
|
|
62
64
|
self._adapter = AsyncRestAdapter(
|
|
63
65
|
config,
|
|
64
|
-
logger=
|
|
66
|
+
logger=self._logger,
|
|
65
67
|
version=version,
|
|
66
68
|
observability=observability,
|
|
67
69
|
session_id=session_id,
|
|
@@ -72,9 +74,6 @@ class AsyncB1Client:
|
|
|
72
74
|
def session_id(self) -> str | None:
|
|
73
75
|
"""
|
|
74
76
|
Retrieves the current SAP session ID.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
str: B1SESSION cookie value or None.
|
|
78
77
|
"""
|
|
79
78
|
return self._adapter.session_id
|
|
80
79
|
|
|
@@ -96,7 +95,6 @@ class AsyncB1Client:
|
|
|
96
95
|
|
|
97
96
|
Note:
|
|
98
97
|
Use ``with`` (sync CM), **not** ``async with``, even in async code.
|
|
99
|
-
This is correct Python — the CM guards a state variable, not I/O.
|
|
100
98
|
"""
|
|
101
99
|
return self._adapter.dry_run(enabled)
|
|
102
100
|
|
|
@@ -117,7 +115,6 @@ class AsyncB1Client:
|
|
|
117
115
|
async def __aenter__(self) -> AsyncB1Client:
|
|
118
116
|
"""
|
|
119
117
|
Entry point for the async context manager.
|
|
120
|
-
Logins and prepares the session.
|
|
121
118
|
"""
|
|
122
119
|
await self.connect()
|
|
123
120
|
return self
|
|
@@ -125,7 +122,6 @@ class AsyncB1Client:
|
|
|
125
122
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
126
123
|
"""
|
|
127
124
|
Exit point for the async context manager.
|
|
128
|
-
Ensures logout and connection pool cleanup.
|
|
129
125
|
"""
|
|
130
126
|
await self.aclose()
|
|
131
127
|
|
|
@@ -149,145 +145,194 @@ class AsyncB1Client:
|
|
|
149
145
|
return DynamicResource(self._adapter)
|
|
150
146
|
|
|
151
147
|
# --------------------------------------------------------------------------
|
|
152
|
-
#
|
|
148
|
+
# Concurrency-Elite Aliases (First-Class Citizens with ETag support)
|
|
153
149
|
# --------------------------------------------------------------------------
|
|
154
150
|
|
|
151
|
+
# --- Master Data ---
|
|
152
|
+
|
|
155
153
|
@property
|
|
156
154
|
def items(self) -> "AsyncGenericResource[Item]":
|
|
157
|
-
"""
|
|
158
|
-
from b1sl.b1sl.models._generated.entities
|
|
159
|
-
|
|
155
|
+
"""Access the 'Items' entity (supports ETags)."""
|
|
156
|
+
from b1sl.b1sl.models._generated.entities import Item
|
|
160
157
|
return self.get_resource(Item, "Items")
|
|
161
158
|
|
|
162
159
|
@property
|
|
163
160
|
def business_partners(self) -> "AsyncGenericResource[BusinessPartner]":
|
|
164
|
-
"""
|
|
165
|
-
from b1sl.b1sl.models._generated.entities
|
|
166
|
-
BusinessPartner,
|
|
167
|
-
)
|
|
168
|
-
|
|
161
|
+
"""Access the 'BusinessPartners' entity (supports ETags)."""
|
|
162
|
+
from b1sl.b1sl.models._generated.entities import BusinessPartner
|
|
169
163
|
return self.get_resource(BusinessPartner, "BusinessPartners")
|
|
170
164
|
|
|
171
165
|
@property
|
|
172
|
-
def
|
|
173
|
-
"""
|
|
174
|
-
from b1sl.b1sl.models._generated.entities
|
|
166
|
+
def activities(self) -> "AsyncGenericResource[Activity]":
|
|
167
|
+
"""Access the 'Activities' entity (supports ETags)."""
|
|
168
|
+
from b1sl.b1sl.models._generated.entities import Activity
|
|
169
|
+
return self.get_resource(Activity, "Activities")
|
|
175
170
|
|
|
176
|
-
|
|
171
|
+
# --- Sales Documents ---
|
|
177
172
|
|
|
178
173
|
@property
|
|
179
174
|
def quotations(self) -> "AsyncGenericResource[Document]":
|
|
180
|
-
"""
|
|
175
|
+
"""Access the 'Quotations' entity (supports ETags)."""
|
|
181
176
|
from b1sl.b1sl.models._generated.entities.general import Document
|
|
182
|
-
|
|
183
177
|
return self.get_resource(Document, "Quotations")
|
|
184
178
|
|
|
185
179
|
@property
|
|
186
180
|
def orders(self) -> "AsyncGenericResource[Document]":
|
|
187
|
-
"""
|
|
181
|
+
"""Access the 'Orders' entity (supports ETags)."""
|
|
188
182
|
from b1sl.b1sl.models._generated.entities.general import Document
|
|
189
|
-
|
|
190
183
|
return self.get_resource(Document, "Orders")
|
|
191
184
|
|
|
192
185
|
@property
|
|
193
186
|
def delivery_notes(self) -> "AsyncGenericResource[Document]":
|
|
194
|
-
"""
|
|
187
|
+
"""Access the 'DeliveryNotes' entity (supports ETags)."""
|
|
195
188
|
from b1sl.b1sl.models._generated.entities.general import Document
|
|
196
|
-
|
|
197
189
|
return self.get_resource(Document, "DeliveryNotes")
|
|
198
190
|
|
|
199
191
|
@property
|
|
200
|
-
def
|
|
201
|
-
"""
|
|
192
|
+
def invoices(self) -> "AsyncGenericResource[Document]":
|
|
193
|
+
"""Access the 'Invoices' entity (supports ETags)."""
|
|
202
194
|
from b1sl.b1sl.models._generated.entities.general import Document
|
|
203
|
-
|
|
204
|
-
return self.get_resource(Document, "PurchaseOrders")
|
|
195
|
+
return self.get_resource(Document, "Invoices")
|
|
205
196
|
|
|
206
197
|
@property
|
|
207
|
-
def
|
|
208
|
-
"""
|
|
198
|
+
def returns(self) -> "AsyncGenericResource[Document]":
|
|
199
|
+
"""Access the 'Returns' entity (supports ETags)."""
|
|
209
200
|
from b1sl.b1sl.models._generated.entities.general import Document
|
|
201
|
+
return self.get_resource(Document, "Returns")
|
|
210
202
|
|
|
211
|
-
|
|
203
|
+
@property
|
|
204
|
+
def return_request(self) -> "AsyncGenericResource[Document]":
|
|
205
|
+
"""Access the 'ReturnRequest' entity (supports ETags)."""
|
|
206
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
207
|
+
return self.get_resource(Document, "ReturnRequest")
|
|
212
208
|
|
|
213
209
|
@property
|
|
214
|
-
def
|
|
215
|
-
"""
|
|
210
|
+
def credit_notes(self) -> "AsyncGenericResource[Document]":
|
|
211
|
+
"""Access the 'CreditNotes' entity (supports ETags)."""
|
|
216
212
|
from b1sl.b1sl.models._generated.entities.general import Document
|
|
213
|
+
return self.get_resource(Document, "CreditNotes")
|
|
217
214
|
|
|
218
|
-
|
|
215
|
+
@property
|
|
216
|
+
def down_payments(self) -> "AsyncGenericResource[Document]":
|
|
217
|
+
"""Access the 'DownPayments' entity (supports ETags)."""
|
|
218
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
219
|
+
return self.get_resource(Document, "DownPayments")
|
|
219
220
|
|
|
220
221
|
@property
|
|
221
|
-
def
|
|
222
|
-
"""
|
|
223
|
-
from b1sl.b1sl.models._generated.entities.general import
|
|
222
|
+
def goods_return_request(self) -> "AsyncGenericResource[Document]":
|
|
223
|
+
"""Access the 'GoodsReturnRequest' entity (supports ETags)."""
|
|
224
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
225
|
+
return self.get_resource(Document, "GoodsReturnRequest")
|
|
226
|
+
|
|
227
|
+
# --- Purchasing Documents ---
|
|
224
228
|
|
|
225
|
-
|
|
229
|
+
@property
|
|
230
|
+
def purchase_requests(self) -> "AsyncGenericResource[Document]":
|
|
231
|
+
"""Access the 'PurchaseRequests' entity (supports ETags)."""
|
|
232
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
233
|
+
return self.get_resource(Document, "PurchaseRequests")
|
|
226
234
|
|
|
227
235
|
@property
|
|
228
|
-
def
|
|
229
|
-
"""
|
|
230
|
-
from b1sl.b1sl.models._generated.entities.general import
|
|
236
|
+
def purchase_quotations(self) -> "AsyncGenericResource[Document]":
|
|
237
|
+
"""Access the 'PurchaseQuotations' entity (supports ETags)."""
|
|
238
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
239
|
+
return self.get_resource(Document, "PurchaseQuotations")
|
|
231
240
|
|
|
232
|
-
|
|
241
|
+
@property
|
|
242
|
+
def purchase_orders(self) -> "AsyncGenericResource[Document]":
|
|
243
|
+
"""Access the 'PurchaseOrders' entity (supports ETags)."""
|
|
244
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
245
|
+
return self.get_resource(Document, "PurchaseOrders")
|
|
233
246
|
|
|
234
247
|
@property
|
|
235
|
-
def
|
|
236
|
-
"""
|
|
237
|
-
from b1sl.b1sl.models._generated.entities.general import
|
|
248
|
+
def purchase_delivery_notes(self) -> "AsyncGenericResource[Document]":
|
|
249
|
+
"""Access the 'PurchaseDeliveryNotes' entity (supports ETags)."""
|
|
250
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
251
|
+
return self.get_resource(Document, "PurchaseDeliveryNotes")
|
|
238
252
|
|
|
239
|
-
|
|
253
|
+
@property
|
|
254
|
+
def purchase_invoices(self) -> "AsyncGenericResource[Document]":
|
|
255
|
+
"""Access the 'PurchaseInvoices' entity (supports ETags)."""
|
|
256
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
257
|
+
return self.get_resource(Document, "PurchaseInvoices")
|
|
240
258
|
|
|
241
|
-
|
|
259
|
+
@property
|
|
260
|
+
def purchase_returns(self) -> "AsyncGenericResource[Document]":
|
|
261
|
+
"""Access the 'PurchaseReturns' entity (supports ETags)."""
|
|
262
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
263
|
+
return self.get_resource(Document, "PurchaseReturns")
|
|
242
264
|
|
|
243
265
|
@property
|
|
244
|
-
def
|
|
245
|
-
"""
|
|
246
|
-
from b1sl.b1sl.models._generated.entities.
|
|
266
|
+
def purchase_credit_notes(self) -> "AsyncGenericResource[Document]":
|
|
267
|
+
"""Access the 'PurchaseCreditNotes' entity (supports ETags)."""
|
|
268
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
269
|
+
return self.get_resource(Document, "PurchaseCreditNotes")
|
|
247
270
|
|
|
248
|
-
|
|
271
|
+
@property
|
|
272
|
+
def purchase_down_payments(self) -> "AsyncGenericResource[Document]":
|
|
273
|
+
"""Access the 'PurchaseDownPayments' entity (supports ETags)."""
|
|
274
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
275
|
+
return self.get_resource(Document, "PurchaseDownPayments")
|
|
249
276
|
|
|
250
|
-
# ---
|
|
277
|
+
# --- Inventory & Specialized ---
|
|
251
278
|
|
|
252
279
|
@property
|
|
253
|
-
def
|
|
254
|
-
"""
|
|
255
|
-
from b1sl.b1sl.models._generated.entities.
|
|
280
|
+
def inventory_gen_entries(self) -> "AsyncGenericResource[Document]":
|
|
281
|
+
"""Access the 'InventoryGenEntries' entity (supports ETags)."""
|
|
282
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
283
|
+
return self.get_resource(Document, "InventoryGenEntries")
|
|
256
284
|
|
|
257
|
-
|
|
285
|
+
@property
|
|
286
|
+
def inventory_gen_exits(self) -> "AsyncGenericResource[Document]":
|
|
287
|
+
"""Access the 'InventoryGenExits' entity (supports ETags)."""
|
|
288
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
289
|
+
return self.get_resource(Document, "InventoryGenExits")
|
|
258
290
|
|
|
259
|
-
|
|
291
|
+
@property
|
|
292
|
+
def drafts(self) -> "AsyncGenericResource[Document]":
|
|
293
|
+
"""Access the 'Drafts' entity (supports ETags)."""
|
|
294
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
295
|
+
return self.get_resource(Document, "Drafts")
|
|
260
296
|
|
|
261
297
|
@property
|
|
262
|
-
def
|
|
263
|
-
"""
|
|
264
|
-
from b1sl.b1sl.models.
|
|
298
|
+
def additional_expenses(self) -> "AsyncGenericResource[B1Model]":
|
|
299
|
+
"""Access the 'AdditionalExpenses' entity (supports ETags)."""
|
|
300
|
+
from b1sl.b1sl.models.base import B1Model
|
|
301
|
+
return self.get_resource(B1Model, "AdditionalExpenses")
|
|
265
302
|
|
|
266
|
-
|
|
303
|
+
# --- Correction marketing documents ---
|
|
267
304
|
|
|
268
|
-
|
|
305
|
+
@property
|
|
306
|
+
def correction_invoice(self) -> "AsyncGenericResource[Document]":
|
|
307
|
+
"""Access the 'CorrectionInvoice' entity (supports ETags)."""
|
|
308
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
309
|
+
return self.get_resource(Document, "CorrectionInvoice")
|
|
269
310
|
|
|
270
311
|
@property
|
|
271
|
-
def
|
|
272
|
-
"""
|
|
273
|
-
from b1sl.b1sl.models._generated.entities.
|
|
312
|
+
def correction_invoice_reversal(self) -> "AsyncGenericResource[Document]":
|
|
313
|
+
"""Access the 'CorrectionInvoiceReversal' entity (supports ETags)."""
|
|
314
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
315
|
+
return self.get_resource(Document, "CorrectionInvoiceReversal")
|
|
274
316
|
|
|
275
|
-
|
|
317
|
+
@property
|
|
318
|
+
def correction_purchase_invoice(self) -> "AsyncGenericResource[Document]":
|
|
319
|
+
"""Access the 'CorrectionPurchaseInvoice' entity (supports ETags)."""
|
|
320
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
321
|
+
return self.get_resource(Document, "CorrectionPurchaseInvoice")
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def correction_purchase_invoice_reversal(self) -> "AsyncGenericResource[Document]":
|
|
325
|
+
"""Access the 'CorrectionPurchaseInvoiceReversal' entity (supports ETags)."""
|
|
326
|
+
from b1sl.b1sl.models._generated.entities.general import Document
|
|
327
|
+
return self.get_resource(Document, "CorrectionPurchaseInvoiceReversal")
|
|
276
328
|
|
|
277
329
|
def udo(self, table_name: str) -> "AsyncUDOResource":
|
|
278
330
|
"""
|
|
279
331
|
Asynchronously access a User Defined Object (UDO) or User Table.
|
|
280
332
|
|
|
281
333
|
AI Role: Dynamic accessor for entities not pre-defined in the client.
|
|
282
|
-
|
|
283
|
-
Args:
|
|
284
|
-
table_name (str): The UDO name in SAP B1.
|
|
285
|
-
|
|
286
|
-
Returns:
|
|
287
|
-
AsyncUDOResource: A resource object bound to the UDO.
|
|
288
334
|
"""
|
|
289
335
|
from b1sl.b1sl.resources.udo import AsyncUDOResource
|
|
290
|
-
|
|
291
336
|
return AsyncUDOResource(adapter=self._adapter, table_name=table_name)
|
|
292
337
|
|
|
293
338
|
|
b1sl/b1sl/async_rest_adapter.py
CHANGED
|
@@ -275,7 +275,10 @@ class AsyncRestAdapter(BaseRestAdapter):
|
|
|
275
275
|
self._raise_if_concurrency_error(
|
|
276
276
|
e.response.status_code, sap_code, sap_msg, endpoint_path, body
|
|
277
277
|
)
|
|
278
|
-
|
|
278
|
+
|
|
279
|
+
# Use specialized exception based on status code if available
|
|
280
|
+
exc_cls = _HTTP_STATUS_TO_EXC.get(e.response.status_code, B1Exception)
|
|
281
|
+
raise exc_cls(f"SAP Error {sap_code}: {sap_msg}", details=body) from e
|
|
279
282
|
except Exception as e:
|
|
280
283
|
exc_captured = e
|
|
281
284
|
raise B1Exception(f"Request failed: {e}") from e
|
|
@@ -290,6 +293,10 @@ class AsyncRestAdapter(BaseRestAdapter):
|
|
|
290
293
|
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
291
294
|
status_code = response.status_code if response is not None else None
|
|
292
295
|
|
|
296
|
+
# Prepare context extras
|
|
297
|
+
context_extras = dict(self._obs.context_extras)
|
|
298
|
+
context_extras["is_dry_run"] = self._dry_run_active and http_method in {"POST", "PATCH", "DELETE"} and not _is_login
|
|
299
|
+
|
|
293
300
|
ctx = HookContext(
|
|
294
301
|
req_id=req_id,
|
|
295
302
|
http_method=http_method,
|
|
@@ -300,7 +307,9 @@ class AsyncRestAdapter(BaseRestAdapter):
|
|
|
300
307
|
user=self._username,
|
|
301
308
|
status_code=status_code,
|
|
302
309
|
duration_ms=duration_ms,
|
|
303
|
-
|
|
310
|
+
payload=log_data if http_method in {"POST", "PATCH"} else None,
|
|
311
|
+
if_match=headers.get("If-Match"),
|
|
312
|
+
extra=context_extras,
|
|
304
313
|
exc=exc_captured,
|
|
305
314
|
)
|
|
306
315
|
|
b1sl/b1sl/base_adapter.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from b1sl.b1sl.config import B1Config
|
|
9
9
|
import asyncio
|
|
10
|
+
import json
|
|
10
11
|
import logging
|
|
11
12
|
import uuid
|
|
12
13
|
from collections import OrderedDict
|
|
@@ -56,6 +57,8 @@ class HookContext:
|
|
|
56
57
|
user: str
|
|
57
58
|
status_code: Optional[int] # None if network exception occurred
|
|
58
59
|
duration_ms: float
|
|
60
|
+
payload: Optional[Dict[str, Any]] = None # Redacted request body
|
|
61
|
+
if_match: Optional[str] = None # ETag sent in If-Match
|
|
59
62
|
extra: Dict[str, Any] = field(default_factory=dict)
|
|
60
63
|
exc: Optional[Exception] = None
|
|
61
64
|
|
|
@@ -75,6 +78,8 @@ class HookContext:
|
|
|
75
78
|
"user": self.user,
|
|
76
79
|
"status_code": self.status_code,
|
|
77
80
|
"duration_ms": round(self.duration_ms, 3),
|
|
81
|
+
"payload": self.payload,
|
|
82
|
+
"if_match": self.if_match,
|
|
78
83
|
**self.extra,
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -390,6 +395,19 @@ class BaseRestAdapter:
|
|
|
390
395
|
|
|
391
396
|
clean_endpoint = ctx.endpoint.lstrip("/")
|
|
392
397
|
msg = f"[{ctx.req_id}][{ctx.user}] [{ctx.http_method} /{clean_endpoint}]{status_label} ({ctx.duration_ms:.1f}ms){slow_label}"
|
|
398
|
+
|
|
399
|
+
# In Dry Run or Debug mode, we might want to see the body in the main message
|
|
400
|
+
meta_info = []
|
|
401
|
+
if ctx.if_match:
|
|
402
|
+
meta_info.append(f"ETag: {ctx.if_match}")
|
|
403
|
+
|
|
404
|
+
if ctx.extra.get("is_dry_run") and ctx.payload:
|
|
405
|
+
payload_str = json.dumps(ctx.payload)
|
|
406
|
+
meta_info.append(f"Body: {payload_str}")
|
|
407
|
+
|
|
408
|
+
if meta_info:
|
|
409
|
+
msg += f" | {' | '.join(meta_info)}"
|
|
410
|
+
|
|
393
411
|
self._logger.log(level, msg, extra=ctx.to_log_extra())
|
|
394
412
|
|
|
395
413
|
# ── URL Helpers ──────────────────────────────────────────────────────── #
|
|
@@ -419,7 +437,13 @@ class BaseRestAdapter:
|
|
|
419
437
|
return "unknown", http_fallback
|
|
420
438
|
|
|
421
439
|
error_node = body.get("error")
|
|
422
|
-
if not error_node
|
|
440
|
+
if not error_node:
|
|
441
|
+
return "unknown", http_fallback
|
|
442
|
+
|
|
443
|
+
if isinstance(error_node, str):
|
|
444
|
+
return "unknown", error_node
|
|
445
|
+
|
|
446
|
+
if not isinstance(error_node, dict):
|
|
423
447
|
return "unknown", http_fallback
|
|
424
448
|
|
|
425
449
|
sap_code = str(error_node.get("code", "unknown"))
|
|
@@ -435,3 +459,4 @@ class BaseRestAdapter:
|
|
|
435
459
|
sap_message = http_fallback
|
|
436
460
|
|
|
437
461
|
return sap_code, sap_message
|
|
462
|
+
|