mapepire-python 0.2.0__tar.gz → 0.3.1__tar.gz

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.
Files changed (46) hide show
  1. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/PKG-INFO +285 -48
  2. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/README.md +270 -31
  3. mapepire_python-0.3.1/mapepire_python/__init__.py +77 -0
  4. mapepire_python-0.3.1/mapepire_python/async_base_job.py +216 -0
  5. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/asyncio/__init__.py +12 -3
  6. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/asyncio/connection.py +40 -14
  7. mapepire_python-0.3.1/mapepire_python/asyncio/cursor.py +156 -0
  8. mapepire_python-0.3.1/mapepire_python/authentication/kerberosTokenProvider.py +121 -0
  9. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/base_job.py +6 -4
  10. mapepire_python-0.3.1/mapepire_python/client/async_sql_job.py +14 -0
  11. mapepire_python-0.3.1/mapepire_python/client/query.py +205 -0
  12. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/client/sql_job.py +31 -18
  13. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/core/connection.py +7 -2
  14. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/core/cursor.py +48 -21
  15. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/core/exceptions.py +18 -20
  16. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/core/utils.py +38 -9
  17. mapepire_python-0.3.1/mapepire_python/data_types.py +528 -0
  18. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/pool/pool_client.py +42 -14
  19. mapepire_python-0.3.1/mapepire_python/pool/pool_job.py +36 -0
  20. mapepire_python-0.3.1/mapepire_python/pool/pool_query.py +168 -0
  21. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/query_manager.py +0 -3
  22. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/ssl.py +1 -0
  23. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/version.py +2 -2
  24. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/websocket.py +16 -3
  25. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python.egg-info/PKG-INFO +285 -48
  26. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python.egg-info/SOURCES.txt +3 -1
  27. mapepire_python-0.3.1/mapepire_python.egg-info/requires.txt +29 -0
  28. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/pyproject.toml +48 -19
  29. mapepire_python-0.2.0/mapepire_python/__init__.py +0 -49
  30. mapepire_python-0.2.0/mapepire_python/asyncio/cursor.py +0 -99
  31. mapepire_python-0.2.0/mapepire_python/asyncio/utils.py +0 -23
  32. mapepire_python-0.2.0/mapepire_python/client/query.py +0 -190
  33. mapepire_python-0.2.0/mapepire_python/data_types.py +0 -245
  34. mapepire_python-0.2.0/mapepire_python/pool/pool_job.py +0 -296
  35. mapepire_python-0.2.0/mapepire_python/pool/pool_query.py +0 -136
  36. mapepire_python-0.2.0/mapepire_python.egg-info/requires.txt +0 -28
  37. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/LICENSE +0 -0
  38. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/client/__init__.py +0 -0
  39. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/client/websocket_client.py +0 -0
  40. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/core/__init__.py +0 -0
  41. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/pool/__init__.py +0 -0
  42. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/pool/async_websocket_client.py +0 -0
  43. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python/py.typed +0 -0
  44. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python.egg-info/dependency_links.txt +0 -0
  45. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/mapepire_python.egg-info/top_level.txt +0 -0
  46. {mapepire_python-0.2.0 → mapepire_python-0.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: mapepire-python
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Author-email: Adam Shedivy <adam.shedivy@ibm.com>, Irfan Sharif <IrfanSharif@ibm.com>
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -207,41 +207,39 @@ License: Apache License
207
207
  Project-URL: Homepage, https://github.com/Mapepire-IBMi/mapepire-python
208
208
  Project-URL: Repository, https://github.com/Mapepire-IBMi/mapepire-python
209
209
  Project-URL: Changelog, https://github.com/Mapepire-IBMi/mapepire-python/blob/main/CHANGELOG.md
210
+ Project-URL: Documentation, https://mapepire-ibmi.github.io/
210
211
  Classifier: Intended Audience :: Science/Research
211
212
  Classifier: Development Status :: 3 - Alpha
212
213
  Classifier: License :: OSI Approved :: Apache Software License
213
214
  Classifier: Programming Language :: Python :: 3
214
215
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
215
- Requires-Python: >3.9
216
+ Requires-Python: >=3.10
216
217
  Description-Content-Type: text/markdown
217
218
  License-File: LICENSE
218
219
  Requires-Dist: dataclasses-json>=0.6.4
219
220
  Requires-Dist: websockets>=14.0
220
- Requires-Dist: pyee
221
221
  Requires-Dist: pep249abc
222
+ Provides-Extra: kerberos
223
+ Requires-Dist: gssapi; sys_platform != "win32" and extra == "kerberos"
224
+ Requires-Dist: pywin32; sys_platform == "win32" and extra == "kerberos"
222
225
  Provides-Extra: dev
223
- Requires-Dist: ruff; extra == "dev"
224
- Requires-Dist: mypy<1.5,>=1.0; extra == "dev"
225
- Requires-Dist: black<24.0,>=23.0; extra == "dev"
226
- Requires-Dist: isort<5.13,>=5.12; extra == "dev"
227
- Requires-Dist: pytest; extra == "dev"
228
- Requires-Dist: pyee; extra == "dev"
226
+ Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
227
+ Requires-Dist: mypy<2.4,>=1.0; extra == "dev"
228
+ Requires-Dist: black<27.0,>=24.3.0; extra == "dev"
229
+ Requires-Dist: isort<8.1,>=5.12; extra == "dev"
230
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
229
231
  Requires-Dist: pep249abc; extra == "dev"
230
232
  Requires-Dist: pytest-asyncio; extra == "dev"
231
- Requires-Dist: pytest-sphinx; extra == "dev"
232
233
  Requires-Dist: pytest-cov; extra == "dev"
233
234
  Requires-Dist: twine>=1.11.0; extra == "dev"
234
235
  Requires-Dist: build; extra == "dev"
235
236
  Requires-Dist: setuptools; extra == "dev"
236
237
  Requires-Dist: wheel; extra == "dev"
237
- Requires-Dist: Sphinx<7.1.0,>=4.3.0; extra == "dev"
238
- Requires-Dist: furo==2023.7.26; extra == "dev"
239
- Requires-Dist: myst-parser<2.1,>=1.0; extra == "dev"
240
- Requires-Dist: sphinx-copybutton==0.5.2; extra == "dev"
241
- Requires-Dist: sphinx-autobuild==2021.3.14; extra == "dev"
242
- Requires-Dist: sphinx-autodoc-typehints==1.23.3; extra == "dev"
243
238
  Requires-Dist: packaging; extra == "dev"
244
239
  Requires-Dist: pre-commit; extra == "dev"
240
+ Requires-Dist: python-dotenv; extra == "dev"
241
+ Requires-Dist: pytest-env; extra == "dev"
242
+ Dynamic: license-file
245
243
 
246
244
  # mapepire-python
247
245
  <div align="center">
@@ -264,11 +262,18 @@ Requires-Dist: pre-commit; extra == "dev"
264
262
  - [Overview](#overview)
265
263
  - [Setup](#setup)
266
264
  - [Install with `pip`](#install-with-pip)
265
+ - [Optional: Kerberos authentication support](#optional-kerberos-authentication-support)
267
266
  - [Server Component Setup](#server-component-setup)
268
- - [Connection options](#connection-options)
267
+ - [Quick Start](#quick-start)
268
+ - [Other Connection options](#other-connection-options)
269
269
  - [1. Using the `DaemonServer` object](#1-using-the-daemonserver-object)
270
+ - [1.1 Authenticating with Kerberos](#11-authenticating-with-kerberos)
271
+ - [1.1.1 Windows](#111-windows)
272
+ - [1.1.2 Other Platforms](#112-other-platforms)
270
273
  - [2. Passing the connection details as a dictionary](#2-passing-the-connection-details-as-a-dictionary)
271
274
  - [3. Using a config file (`.ini`) to store the connection details](#3-using-a-config-file-ini-to-store-the-connection-details)
275
+ - [4. Using environment variables](#4-using-environment-variables)
276
+ - [TLS Configuration](#tls-configuration)
272
277
  - [Usage](#usage)
273
278
  - [1. Using the `SQLJob` object to run queries synchronously](#1-using-the-sqljob-object-to-run-queries-synchronously)
274
279
  - [Query and run](#query-and-run)
@@ -329,46 +334,147 @@ Requires-Dist: pre-commit; extra == "dev"
329
334
  pip install mapepire-python
330
335
  ```
331
336
 
337
+ #### Optional: Kerberos authentication support
338
+
339
+ Kerberos authentication (see [Authenticating with Kerberos](#11-authenticating-with-kerberos)) requires the optional `kerberos` extra, which pulls in `gssapi` on Linux/macOS or `pywin32` on Windows:
340
+
341
+ ```bash
342
+ pip install mapepire-python[kerberos]
343
+ ```
344
+
345
+ On Linux, building `gssapi` also requires Kerberos development headers to be available, e.g.:
346
+
347
+ ```bash
348
+ sudo apt install libkrb5-dev # Debian/Ubuntu
349
+ sudo dnf install krb5-devel # Fedora/RHEL
350
+ ```
351
+
352
+ If you don't use Kerberos authentication, you don't need this extra — the base `pip install mapepire-python` has no Kerberos dependencies.
353
+
332
354
  ### Server Component Setup
333
355
  To use mapire-python, you will need to have the Mapepire Server Component running on your IBM i server. Follow these instructions to set up the server component: [Mapepire Server Installation](https://mapepire-ibmi.github.io/guides/sysadmin/)
334
356
 
335
-
336
- # Connection options
357
+ # Quick Start
358
+
359
+ To get started with `mapepire-python`, you will need to setup a connection credentials for the Mapepire server. You can use a dictionary to store the connection details:
360
+
361
+ ```python
362
+ from mapepire_python import connect
363
+
364
+ creds = {
365
+ "host": "SERVER",
366
+ "port": 8076,
367
+ "user": "USER",
368
+ "password": "PASSWORD",
369
+ }
370
+
371
+ with connect(creds) as conn:
372
+ with conn.execute("select * from sample.employee") as cursor:
373
+ result = cursor.fetchone()
374
+ print(result)
375
+
376
+ ```
377
+
378
+ # Other Connection options
337
379
 
338
- There are three ways to configure mapepire server connection details using `mapepire-python`:
380
+ > [!NOTE]
381
+ > TLS support as of version 0.3.0 is now available. Server certificate verification is enabled by default. To disable certificate verification, set the `ignoreUnauthorized` field to `True` in the connection details.
382
+ > - To update run `pip install -U mapepire-python`
383
+ >
384
+ > - More info TLS Configuration [here](#tls-configuration)
385
+
386
+ There are four ways to configure mapepire server connection details using `mapepire-python`:
339
387
 
340
388
  1. Using the `DaemonServer` object
341
389
  2. Passing the connection details as a dictionary
342
390
  3. Using a config file (`.ini`) to store the connection details
391
+ 4. Using environment variables
343
392
 
344
393
  ## 1. Using the `DaemonServer` object
345
394
 
346
- to use the `DaemonServer` object, you will need to import the `DaemonServer` class from the `mapepire_python.data_types` module:
395
+ `DaemonServer` is importable directly from `mapepire_python`:
347
396
 
348
397
  ```python
349
- from mapepire_python.data_types import DaemonServer
398
+ from mapepire_python import DaemonServer
350
399
 
351
400
  creds = DaemonServer(
352
401
  host="SERVER",
353
402
  port="PORT",
354
403
  user="USER",
355
- password="PASSWORD",
356
- ignoreUnauthorized=True
404
+ password="PASSWORD"
357
405
  )
358
406
  ```
359
407
 
360
408
  Once you have created the `DaemonServer` object, you can pass it to the `SQLJob` object to connect to the mapepire server:
361
409
 
362
410
  ```python
363
- from mapepire_python.client.sql_job import SQLJob
364
- from mapepire_python.data_types import DaemonServer
411
+ from mapepire_python import SQLJob, DaemonServer
365
412
 
366
413
  creds = DaemonServer(
367
414
  host="SERVER",
368
415
  port="PORT",
369
416
  user="USER",
370
- password="PASSWORD",
371
- ignoreUnauthorized=True
417
+ password="PASSWORD"
418
+ )
419
+
420
+ job = SQLJob(creds)
421
+ ```
422
+
423
+ ### 1.1 Authenticating with Kerberos
424
+ If your IBM i is configured to support Kerberos authentication, you can authenticate using Kerberos instead of passing a plain-text password to the `DaemonServer` Object.
425
+
426
+ > [!NOTE]
427
+ > Kerberos support requires the `kerberos` extra: `pip install mapepire-python[kerberos]`. See [Optional: Kerberos authentication support](#optional-kerberos-authentication-support).
428
+
429
+ #### 1.1.1 Windows
430
+
431
+ If your Windows machine is part of a Kerberos realm and supports SSPI authentication, you can authenticate by creating the `DaemonServer` as shown below:
432
+ ```python
433
+ from mapepire_python import DaemonServer
434
+ from mapepire_python.authentication.kerberosTokenProvider import KerberosTokenProvider
435
+
436
+ creds = DaemonServer(
437
+ host="SERVER",
438
+ password=KerberosTokenProvider(host="SERVER"),
439
+ user="USER",
440
+ port="PORT",
441
+ )
442
+
443
+ job = SQLJob(creds)
444
+ ```
445
+
446
+ #### 1.1.2 Other Platforms
447
+
448
+ For non-Windows platforms, Kerberos authentication requires a valid Ticket Granting Ticket (TGT) in your credential cache.
449
+
450
+ Required Parameters:
451
+
452
+ 1. `host`: The IBM i host you are connecting to
453
+ 2. `realm`: Your Kerberos realm
454
+ 3. `realm_user`: Your Kerberos username
455
+ 4. `krb5_path`: Path to your `krb5.conf` configuration file
456
+
457
+ Optional Parameters:
458
+
459
+ 1. `ticket_cache`: Path to your ticket cache (if not default)
460
+ 2. `krb5_mech`: The Kerberos 5 mechanism to use (if not default)
461
+
462
+ ```python
463
+ from mapepire_python import DaemonServer
464
+ from mapepire_python.authentication.kerberosTokenProvider import KerberosTokenProvider
465
+
466
+ token_provider = KerberosTokenProvider(
467
+ realm="REALM",
468
+ realm_user="REALM_USER",
469
+ host="SERVER",
470
+ krb5_path="KRB5_PATH"
471
+ )
472
+
473
+ creds = DaemonServer(
474
+ host="SERVER",
475
+ password=token_provider,
476
+ user="USER",
477
+ port="PORT",
372
478
  )
373
479
 
374
480
  job = SQLJob(creds)
@@ -379,7 +485,7 @@ job = SQLJob(creds)
379
485
  You can also use a dictionary to configure the connection details:
380
486
 
381
487
  ```python
382
- from mapepire_python.client.sql_job import SQLJob
488
+ from mapepire_python import SQLJob
383
489
 
384
490
  creds = {
385
491
  "host": "SERVER",
@@ -402,27 +508,96 @@ First create a `mapepire.ini` file in the root of your project with the followin
402
508
 
403
509
  ```ini title=mapepire.ini
404
510
  [mapepire]
405
- SERVER="SERVER"
406
- PORT="PORT"
407
- USER="USER"
408
- PASSWORD="PASSWORD"
511
+ host=SERVER
512
+ port=PORT
513
+ user=USER
514
+ password=PASSWORD
409
515
  ```
410
516
 
517
+ > **Note:** The keys must match the `DaemonServer` fields (`host`, `port`, `user`, `password`), and values must **not** be quoted — write `host=myhost`, not `host="myhost"`.
518
+
411
519
  Then you can create a `SQLJob` object by passing the path to the `.ini` file which will handle the connection details
412
520
 
413
521
 
414
522
  ```python
415
- from mapepire_python.client.sql_job import SQLJob
523
+ from mapepire_python import SQLJob
416
524
 
417
525
  job = SQLJob("./mapepire.ini", section="mapepire")
418
526
  ```
419
527
 
420
528
  The `section` argument is optional and allows you to specify a specific section in the `.ini` file where the connection details are stored. This allows you to store multiple connection details to different systems in the same file. If you do not specify a `section`, the first section in the file will be used.
421
529
 
530
+ ## 4. Using environment variables
531
+
532
+ You can configure connection credentials through environment variables, avoiding the need to pass them explicitly in code:
533
+
534
+ | Variable | Required | Default | Description |
535
+ |---|---|---|---|
536
+ | `MAPEPIRE_HOST` | Yes | — | Server hostname |
537
+ | `MAPEPIRE_USER` | Yes | — | Database user |
538
+ | `MAPEPIRE_PASSWORD` | Yes | — | Database password |
539
+ | `MAPEPIRE_PORT` | No | `8076` | Server port |
540
+ | `MAPEPIRE_CA_PATH` | No | — | Path to CA certificate file |
541
+
542
+ Set the variables in your shell:
543
+
544
+ ```bash
545
+ export MAPEPIRE_HOST=myibmi.example.com
546
+ export MAPEPIRE_USER=myuser
547
+ export MAPEPIRE_PASSWORD=mypassword
548
+ ```
549
+
550
+ Then `connect()` and `async_connect()` can be called with no arguments:
551
+
552
+ ```python
553
+ from mapepire_python import connect
554
+
555
+ with connect() as conn:
556
+ with conn.execute("select * from sample.employee") as cursor:
557
+ print(cursor.fetchone())
558
+ ```
559
+
560
+ You can also create a `DaemonServer` directly from env vars:
561
+
562
+ ```python
563
+ from mapepire_python import DaemonServer, SQLJob
564
+
565
+ creds = DaemonServer.from_env()
566
+ job = SQLJob(creds)
567
+ ```
568
+
569
+ ## TLS Configuration
570
+
571
+ Server certificate verification (`ssl.CERT_REQUIRED`) is enabled by default. To disable certificate verification, set the `ignoreUnauthorized` field to `True` in the connection details.
572
+
573
+ get the server certificate:
574
+
575
+ ```python
576
+ from mapepire_python.data_types import DaemonServer
577
+ from mapepire_python.ssl import get_certificate
578
+
579
+ creds = DaemonServer(host=server, port=port, user=user, password=password)
580
+ cert = get_certificate(creds)
581
+ print(cert)
582
+ ```
583
+
584
+ ## Logging
585
+
586
+ `mapepire-python` emits structured logs through the standard library `logging` module. Following library best practice, it does not configure any handlers itself, so logging is silent until your application opts in. Each module logs under the `mapepire_python` namespace, so you can enable and filter logs as needed:
587
+
588
+ ```python
589
+ import logging
590
+
591
+ # Enable INFO-level logs from mapepire-python
592
+ logging.basicConfig(level=logging.INFO)
593
+ logging.getLogger("mapepire_python").setLevel(logging.DEBUG)
594
+ ```
595
+
596
+
422
597
 
423
598
  # Usage
424
599
 
425
- Depending on your setup and use case, you can choose the most convenient way to configure the connection details. The following usage examples are compatible with all three connection options detailed above. For simplicity, we assume there is a `mapepire.ini` file in the root of the project with the connection details.
600
+ Depending on your setup and use case, you can choose the most convenient way to configure the connection details. The following usage examples are compatible with all four connection options detailed above. For simplicity, we assume there is a `mapepire.ini` file in the root of the project with the connection details.
426
601
 
427
602
 
428
603
  There are four main ways to run queries using `mapepire-python`:
@@ -436,7 +611,7 @@ There are four main ways to run queries using `mapepire-python`:
436
611
  ## 1. Using the `SQLJob` object to run queries synchronously
437
612
 
438
613
  ```python
439
- from mapepire_python.client.sql_job import SQLJob
614
+ from mapepire_python import SQLJob
440
615
 
441
616
  with SQLJob("./mapepire.ini") as sql_job:
442
617
  with sql_job.query("select * from sample.employee") as query:
@@ -580,7 +755,7 @@ In the ouput above, the query was successful and returned one row of data.
580
755
  To create and run a query in a single step, use the `query_and_run` method:
581
756
 
582
757
  ```python
583
- from mapepire_python.client.sql_job import SQLJob
758
+ from mapepire_python import SQLJob
584
759
 
585
760
  with SQLJob("./mapepire.ini") as sql_job:
586
761
  # query automatically closed after running
@@ -594,7 +769,7 @@ The `PoolJob` object can be used to create and run queries asynchronously:
594
769
 
595
770
  ```python
596
771
  import asyncio
597
- from mapepire_python.pool.pool_job import PoolJob
772
+ from mapepire_python import PoolJob
598
773
 
599
774
  async def main():
600
775
  async with PoolJob("./mapepire.ini") as pool_job:
@@ -610,7 +785,7 @@ To run a create and run a query asynchronously in a single step, use the `query_
610
785
 
611
786
  ```python
612
787
  import asyncio
613
- from mapepire_python.pool.pool_job import PoolJob
788
+ from mapepire_python import PoolJob
614
789
 
615
790
  async def main():
616
791
  async with PoolJob("./mapepire.ini") as pool_job:
@@ -629,7 +804,7 @@ The `Pool` object can be used to create a pool of `PoolJob` objects to run queri
629
804
 
630
805
  ```python
631
806
  import asyncio
632
- from mapepire_python.pool.pool_client import Pool, PoolOptions
807
+ from mapepire_python import Pool, PoolOptions
633
808
 
634
809
  async def main():
635
810
  async with Pool(
@@ -694,18 +869,57 @@ with connect("./mapepire.ini") as conn:
694
869
 
695
870
  ## PEP 249 Asynchronous Implementation
696
871
 
697
- The PEP 249 implementation also provides an asynchronous interface for running queries. The `connect` function returns an asynchronous context manager that can be used with the `async with` statement:
872
+ The PEP 249 implementation provides a native async interface backed by non-blocking WebSocket I/O no thread delegation. Use `async_connect` from the top-level package, or `connect` from `mapepire_python.asyncio`:
698
873
 
699
874
  ```python
700
875
  import asyncio
701
- from mapepire_python.asycnio import connect
876
+ from mapepire_python import async_connect
702
877
 
703
878
  async def main():
704
- async with connect("./mapepire.ini") as conn:
879
+ async with async_connect("./mapepire.ini") as conn:
705
880
  async with await conn.execute("select * from sample.employee") as cursor:
706
881
  result = await cursor.fetchone()
707
882
  print(result)
708
-
883
+
884
+ if __name__ == '__main__':
885
+ asyncio.run(main())
886
+ ```
887
+
888
+ Fetch multiple rows at once with `fetchmany` or drain the full result set with `fetchall`:
889
+
890
+ ```python
891
+ async def main():
892
+ async with async_connect("./mapepire.ini") as conn:
893
+ async with await conn.execute("select * from sample.employee") as cursor:
894
+ page = await cursor.fetchmany(10) # first 10 rows
895
+ rest = await cursor.fetchall() # remaining rows
896
+
897
+ if __name__ == '__main__':
898
+ asyncio.run(main())
899
+ ```
900
+
901
+ Stream rows one at a time using `async for`:
902
+
903
+ ```python
904
+ async def main():
905
+ async with async_connect("./mapepire.ini") as conn:
906
+ async for row in await conn.execute("select * from sample.employee"):
907
+ print(row)
908
+
909
+ if __name__ == '__main__':
910
+ asyncio.run(main())
911
+ ```
912
+
913
+ Use `AsyncSQLJob` directly for lower-level access without a pool:
914
+
915
+ ```python
916
+ from mapepire_python.client.async_sql_job import AsyncSQLJob
917
+
918
+ async def main():
919
+ async with AsyncSQLJob("./mapepire.ini") as job:
920
+ result = await job.query_and_run("select * from sample.employee")
921
+ print(result.data)
922
+
709
923
  if __name__ == '__main__':
710
924
  asyncio.run(main())
711
925
  ```
@@ -863,9 +1077,26 @@ conda deactivate
863
1077
  ```
864
1078
  ## Run local test suite
865
1079
 
866
- First, create a `pytest.ini` file in the `tests` directory.
1080
+ The suite is split in two:
1081
+
1082
+ - `tests/unit/` — offline unit tests. They mock the WebSocket transport and need
1083
+ **no** IBM i server or credentials, so they run anywhere (including CI on forks).
1084
+ - `tests/integration/` — live-server tests. They connect to a real Mapepire
1085
+ server and require credentials.
867
1086
 
868
- `tests/pytest.ini`
1087
+ ### Unit tests (no server required)
1088
+
1089
+ ```bash
1090
+ # activate python development environment first
1091
+
1092
+ pytest tests/unit/
1093
+ ```
1094
+
1095
+ ### Integration tests (server required)
1096
+
1097
+ First, create a `pytest.ini` file in the `tests/integration` directory:
1098
+
1099
+ `tests/integration/pytest.ini`
869
1100
 
870
1101
  ```ini
871
1102
  [pytest]
@@ -875,11 +1106,17 @@ env =
875
1106
  VITE_DB_PASS=PASS
876
1107
  ```
877
1108
 
878
- Run the test suite from the `mapepire-python` directory:
1109
+ Then run the integration suite from the `mapepire-python` directory:
879
1110
 
880
1111
  ```bash
881
1112
  # activate python development environment first
882
1113
 
1114
+ pytest tests/integration/
1115
+ ```
1116
+
1117
+ To run everything (unit + integration), just point pytest at `tests/`:
1118
+
1119
+ ```bash
883
1120
  pytest tests/
884
1121
  ```
885
1122