mapepire-python 0.1.8__tar.gz → 0.3.0__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 (47) hide show
  1. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/PKG-INFO +263 -44
  2. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/README.md +249 -26
  3. mapepire_python-0.3.0/mapepire_python/__init__.py +77 -0
  4. mapepire_python-0.3.0/mapepire_python/async_base_job.py +216 -0
  5. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/asyncio/__init__.py +12 -3
  6. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/asyncio/connection.py +40 -14
  7. mapepire_python-0.3.0/mapepire_python/asyncio/cursor.py +156 -0
  8. mapepire_python-0.3.0/mapepire_python/authentication/kerberosTokenProvider.py +115 -0
  9. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/base_job.py +15 -5
  10. mapepire_python-0.3.0/mapepire_python/client/async_sql_job.py +14 -0
  11. mapepire_python-0.3.0/mapepire_python/client/query.py +205 -0
  12. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/client/sql_job.py +39 -21
  13. mapepire_python-0.3.0/mapepire_python/client/websocket_client.py +20 -0
  14. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/core/connection.py +7 -2
  15. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/core/cursor.py +50 -24
  16. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/core/exceptions.py +18 -20
  17. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/core/utils.py +38 -9
  18. mapepire_python-0.3.0/mapepire_python/data_types.py +528 -0
  19. mapepire_python-0.3.0/mapepire_python/pool/async_websocket_client.py +18 -0
  20. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/pool/pool_client.py +42 -14
  21. mapepire_python-0.3.0/mapepire_python/pool/pool_job.py +36 -0
  22. mapepire_python-0.3.0/mapepire_python/pool/pool_query.py +168 -0
  23. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/query_manager.py +0 -3
  24. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/ssl.py +7 -1
  25. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/version.py +2 -2
  26. mapepire_python-0.3.0/mapepire_python/websocket.py +74 -0
  27. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python.egg-info/PKG-INFO +263 -44
  28. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python.egg-info/SOURCES.txt +6 -2
  29. mapepire_python-0.3.0/mapepire_python.egg-info/requires.txt +25 -0
  30. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/pyproject.toml +45 -19
  31. mapepire_python-0.1.8/mapepire_python/__init__.py +0 -49
  32. mapepire_python-0.1.8/mapepire_python/asyncio/cursor.py +0 -99
  33. mapepire_python-0.1.8/mapepire_python/asyncio/utils.py +0 -23
  34. mapepire_python-0.1.8/mapepire_python/client/query.py +0 -185
  35. mapepire_python-0.1.8/mapepire_python/client/websocket.py +0 -40
  36. mapepire_python-0.1.8/mapepire_python/data_types.py +0 -245
  37. mapepire_python-0.1.8/mapepire_python/pool/pool_job.py +0 -318
  38. mapepire_python-0.1.8/mapepire_python/pool/pool_query.py +0 -136
  39. mapepire_python-0.1.8/mapepire_python.egg-info/requires.txt +0 -29
  40. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/LICENSE +0 -0
  41. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/client/__init__.py +0 -0
  42. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/core/__init__.py +0 -0
  43. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/pool/__init__.py +0 -0
  44. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python/py.typed +0 -0
  45. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python.egg-info/dependency_links.txt +0 -0
  46. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/mapepire_python.egg-info/top_level.txt +0 -0
  47. {mapepire_python-0.1.8 → mapepire_python-0.3.0}/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.1.8
3
+ Version: 0.3.0
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,42 +207,38 @@ 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
- Requires-Dist: websocket-client>=1.2.1
220
- Requires-Dist: websockets==13.1
221
- Requires-Dist: pyee
220
+ Requires-Dist: websockets>=14.0
222
221
  Requires-Dist: pep249abc
223
222
  Provides-Extra: dev
224
223
  Requires-Dist: ruff; extra == "dev"
225
- Requires-Dist: mypy<1.5,>=1.0; extra == "dev"
226
- Requires-Dist: black<24.0,>=23.0; extra == "dev"
227
- Requires-Dist: isort<5.13,>=5.12; extra == "dev"
228
- Requires-Dist: pytest; extra == "dev"
229
- Requires-Dist: pyee; extra == "dev"
224
+ Requires-Dist: mypy<1.20,>=1.0; extra == "dev"
225
+ Requires-Dist: black<27.0,>=24.3.0; extra == "dev"
226
+ Requires-Dist: isort<6.2,>=5.12; extra == "dev"
227
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
230
228
  Requires-Dist: pep249abc; extra == "dev"
231
229
  Requires-Dist: pytest-asyncio; extra == "dev"
232
- Requires-Dist: pytest-sphinx; extra == "dev"
233
230
  Requires-Dist: pytest-cov; extra == "dev"
234
231
  Requires-Dist: twine>=1.11.0; extra == "dev"
235
232
  Requires-Dist: build; extra == "dev"
236
233
  Requires-Dist: setuptools; extra == "dev"
237
234
  Requires-Dist: wheel; extra == "dev"
238
- Requires-Dist: Sphinx<7.1.0,>=4.3.0; extra == "dev"
239
- Requires-Dist: furo==2023.7.26; extra == "dev"
240
- Requires-Dist: myst-parser<2.1,>=1.0; extra == "dev"
241
- Requires-Dist: sphinx-copybutton==0.5.2; extra == "dev"
242
- Requires-Dist: sphinx-autobuild==2021.3.14; extra == "dev"
243
- Requires-Dist: sphinx-autodoc-typehints==1.23.3; extra == "dev"
244
235
  Requires-Dist: packaging; extra == "dev"
245
236
  Requires-Dist: pre-commit; extra == "dev"
237
+ Requires-Dist: python-dotenv; extra == "dev"
238
+ Requires-Dist: pytest-env; extra == "dev"
239
+ Requires-Dist: gssapi; extra == "dev"
240
+ Requires-Dist: pywin32; sys_platform == "win32" and extra == "dev"
241
+ Dynamic: license-file
246
242
 
247
243
  # mapepire-python
248
244
  <div align="center">
@@ -266,10 +262,16 @@ Requires-Dist: pre-commit; extra == "dev"
266
262
  - [Setup](#setup)
267
263
  - [Install with `pip`](#install-with-pip)
268
264
  - [Server Component Setup](#server-component-setup)
269
- - [Connection options](#connection-options)
265
+ - [Quick Start](#quick-start)
266
+ - [Other Connection options](#other-connection-options)
270
267
  - [1. Using the `DaemonServer` object](#1-using-the-daemonserver-object)
268
+ - [1.1 Authenticating with Kerberos](#11-authenticating-with-kerberos)
269
+ - [1.1.1 Windows](#111-windows)
270
+ - [1.1.2 Other Platforms](#112-other-platforms)
271
271
  - [2. Passing the connection details as a dictionary](#2-passing-the-connection-details-as-a-dictionary)
272
272
  - [3. Using a config file (`.ini`) to store the connection details](#3-using-a-config-file-ini-to-store-the-connection-details)
273
+ - [4. Using environment variables](#4-using-environment-variables)
274
+ - [TLS Configuration](#tls-configuration)
273
275
  - [Usage](#usage)
274
276
  - [1. Using the `SQLJob` object to run queries synchronously](#1-using-the-sqljob-object-to-run-queries-synchronously)
275
277
  - [Query and run](#query-and-run)
@@ -315,6 +317,13 @@ Requires-Dist: pre-commit; extra == "dev"
315
317
 
316
318
  `mapepire-python` requires Python 3.10 or later.
317
319
 
320
+ > [!NOTE]
321
+ > New websocket Implementation: As of version 0.2.0, `mapepire-python` uses the `websockets` library for websocket connections. If you are upgrading from a previous version, make sure to update your dependecies. The `websocket-client` library is no longer supported.
322
+ > - To update run `pip install -U mapepire-python`
323
+ >
324
+ > - More info on [websockets](https://websockets.readthedocs.io/en/stable/)
325
+
326
+
318
327
  ### Install with `pip`
319
328
 
320
329
  `mapepire-python` is available on [PyPi](https://pypi.org/project/mapepire-python/). Just Run
@@ -326,43 +335,124 @@ pip install mapepire-python
326
335
  ### Server Component Setup
327
336
  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/)
328
337
 
329
-
330
- # Connection options
338
+ # Quick Start
339
+
340
+ 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:
341
+
342
+ ```python
343
+ from mapepire_python import connect
344
+
345
+ creds = {
346
+ "host": "SERVER",
347
+ "port": 8076,
348
+ "user": "USER",
349
+ "password": "PASSWORD",
350
+ }
351
+
352
+ with connect(creds) as conn:
353
+ with conn.execute("select * from sample.employee") as cursor:
354
+ result = cursor.fetchone()
355
+ print(result)
356
+
357
+ ```
358
+
359
+ # Other Connection options
360
+
361
+ > [!NOTE]
362
+ > 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.
363
+ > - To update run `pip install -U mapepire-python`
364
+ >
365
+ > - More info TLS Configuration [here](#tls-configuration)
331
366
 
332
- There are three ways to configure mapepire server connection details using `mapepire-python`:
367
+ There are four ways to configure mapepire server connection details using `mapepire-python`:
333
368
 
334
369
  1. Using the `DaemonServer` object
335
370
  2. Passing the connection details as a dictionary
336
371
  3. Using a config file (`.ini`) to store the connection details
372
+ 4. Using environment variables
337
373
 
338
374
  ## 1. Using the `DaemonServer` object
339
375
 
340
- to use the `DaemonServer` object, you will need to import the `DaemonServer` class from the `mapepire_python.data_types` module:
376
+ `DaemonServer` is importable directly from `mapepire_python`:
341
377
 
342
378
  ```python
343
- from mapepire_python.data_types import DaemonServer
379
+ from mapepire_python import DaemonServer
344
380
 
345
381
  creds = DaemonServer(
346
382
  host="SERVER",
347
383
  port="PORT",
348
384
  user="USER",
349
- password="PASSWORD",
350
- ignoreUnauthorized=True
385
+ password="PASSWORD"
351
386
  )
352
387
  ```
353
388
 
354
389
  Once you have created the `DaemonServer` object, you can pass it to the `SQLJob` object to connect to the mapepire server:
355
390
 
356
391
  ```python
357
- from mapepire_python.client.sql_job import SQLJob
358
- from mapepire_python.data_types import DaemonServer
392
+ from mapepire_python import SQLJob, DaemonServer
393
+
394
+ creds = DaemonServer(
395
+ host="SERVER",
396
+ port="PORT",
397
+ user="USER",
398
+ password="PASSWORD"
399
+ )
400
+
401
+ job = SQLJob(creds)
402
+ ```
403
+
404
+ ### 1.1 Authenticating with Kerberos
405
+ 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.
406
+
407
+ #### 1.1.1 Windows
408
+
409
+ If your Windows machine is part of a Kerberos realm and supports SSPI authentication, you can authenticate by creating the `DaemonServer` as shown below:
410
+ ```python
411
+ from mapepire_python import DaemonServer
412
+ from mapepire_python.authentication.kerberosTokenProvider import KerberosTokenProvider
359
413
 
360
414
  creds = DaemonServer(
361
415
  host="SERVER",
416
+ password=KerberosTokenProvider(host="SERVER"),
417
+ user="USER",
362
418
  port="PORT",
419
+ )
420
+
421
+ job = SQLJob(creds)
422
+ ```
423
+
424
+ #### 1.1.2 Other Platforms
425
+
426
+ For non-Windows platforms, Kerberos authentication requires a valid Ticket Granting Ticket (TGT) in your credential cache.
427
+
428
+ Required Parameters:
429
+
430
+ 1. `host`: The IBM i host you are connecting to
431
+ 2. `realm`: Your Kerberos realm
432
+ 3. `realm_user`: Your Kerberos username
433
+ 4. `krb5_path`: Path to your `krb5.conf` configuration file
434
+
435
+ Optional Parameters:
436
+
437
+ 1. `ticket_cache`: Path to your ticket cache (if not default)
438
+ 2. `krb5_mech`: The Kerberos 5 mechanism to use (if not default)
439
+
440
+ ```python
441
+ from mapepire_python import DaemonServer
442
+ from mapepire_python.authentication.kerberosTokenProvider import KerberosTokenProvider
443
+
444
+ token_provider = KerberosTokenProvider(
445
+ realm="REALM",
446
+ realm_user="REALM_USER",
447
+ host="SERVER",
448
+ krb5_path="KRB5_PATH"
449
+ )
450
+
451
+ creds = DaemonServer(
452
+ host="SERVER",
453
+ password=token_provider,
363
454
  user="USER",
364
- password="PASSWORD",
365
- ignoreUnauthorized=True
455
+ port="PORT",
366
456
  )
367
457
 
368
458
  job = SQLJob(creds)
@@ -373,7 +463,7 @@ job = SQLJob(creds)
373
463
  You can also use a dictionary to configure the connection details:
374
464
 
375
465
  ```python
376
- from mapepire_python.client.sql_job import SQLJob
466
+ from mapepire_python import SQLJob
377
467
 
378
468
  creds = {
379
469
  "host": "SERVER",
@@ -406,13 +496,80 @@ Then you can create a `SQLJob` object by passing the path to the `.ini` file whi
406
496
 
407
497
 
408
498
  ```python
409
- from mapepire_python.client.sql_job import SQLJob
499
+ from mapepire_python import SQLJob
410
500
 
411
501
  job = SQLJob("./mapepire.ini", section="mapepire")
412
502
  ```
413
503
 
414
504
  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.
415
505
 
506
+ ## 4. Using environment variables
507
+
508
+ You can configure connection credentials through environment variables, avoiding the need to pass them explicitly in code:
509
+
510
+ | Variable | Required | Default | Description |
511
+ |---|---|---|---|
512
+ | `MAPEPIRE_HOST` | Yes | — | Server hostname |
513
+ | `MAPEPIRE_USER` | Yes | — | Database user |
514
+ | `MAPEPIRE_PASSWORD` | Yes | — | Database password |
515
+ | `MAPEPIRE_PORT` | No | `8076` | Server port |
516
+ | `MAPEPIRE_CA_PATH` | No | — | Path to CA certificate file |
517
+
518
+ Set the variables in your shell:
519
+
520
+ ```bash
521
+ export MAPEPIRE_HOST=myibmi.example.com
522
+ export MAPEPIRE_USER=myuser
523
+ export MAPEPIRE_PASSWORD=mypassword
524
+ ```
525
+
526
+ Then `connect()` and `async_connect()` can be called with no arguments:
527
+
528
+ ```python
529
+ from mapepire_python import connect
530
+
531
+ with connect() as conn:
532
+ with conn.execute("select * from sample.employee") as cursor:
533
+ print(cursor.fetchone())
534
+ ```
535
+
536
+ You can also create a `DaemonServer` directly from env vars:
537
+
538
+ ```python
539
+ from mapepire_python import DaemonServer, SQLJob
540
+
541
+ creds = DaemonServer.from_env()
542
+ job = SQLJob(creds)
543
+ ```
544
+
545
+ ## TLS Configuration
546
+
547
+ Server certificate verification (`ssl.CERT_REQUIRED`) is enabled by default. To disable certificate verification, set the `ignoreUnauthorized` field to `True` in the connection details.
548
+
549
+ get the server certificate:
550
+
551
+ ```python
552
+ from mapepire_python.data_types import DaemonServer
553
+ from mapepire_python.ssl import get_certificate
554
+
555
+ creds = DaemonServer(host=server, port=port, user=user, password=password)
556
+ cert = get_certificate(creds)
557
+ print(cert)
558
+ ```
559
+
560
+ ## Logging
561
+
562
+ `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:
563
+
564
+ ```python
565
+ import logging
566
+
567
+ # Enable INFO-level logs from mapepire-python
568
+ logging.basicConfig(level=logging.INFO)
569
+ logging.getLogger("mapepire_python").setLevel(logging.DEBUG)
570
+ ```
571
+
572
+
416
573
 
417
574
  # Usage
418
575
 
@@ -430,7 +587,7 @@ There are four main ways to run queries using `mapepire-python`:
430
587
  ## 1. Using the `SQLJob` object to run queries synchronously
431
588
 
432
589
  ```python
433
- from mapepire_python.client.sql_job import SQLJob
590
+ from mapepire_python import SQLJob
434
591
 
435
592
  with SQLJob("./mapepire.ini") as sql_job:
436
593
  with sql_job.query("select * from sample.employee") as query:
@@ -574,7 +731,7 @@ In the ouput above, the query was successful and returned one row of data.
574
731
  To create and run a query in a single step, use the `query_and_run` method:
575
732
 
576
733
  ```python
577
- from mapepire_python.client.sql_job import SQLJob
734
+ from mapepire_python import SQLJob
578
735
 
579
736
  with SQLJob("./mapepire.ini") as sql_job:
580
737
  # query automatically closed after running
@@ -588,7 +745,7 @@ The `PoolJob` object can be used to create and run queries asynchronously:
588
745
 
589
746
  ```python
590
747
  import asyncio
591
- from mapepire_python.pool.pool_job import PoolJob
748
+ from mapepire_python import PoolJob
592
749
 
593
750
  async def main():
594
751
  async with PoolJob("./mapepire.ini") as pool_job:
@@ -604,7 +761,7 @@ To run a create and run a query asynchronously in a single step, use the `query_
604
761
 
605
762
  ```python
606
763
  import asyncio
607
- from mapepire_python.pool.pool_job import PoolJob
764
+ from mapepire_python import PoolJob
608
765
 
609
766
  async def main():
610
767
  async with PoolJob("./mapepire.ini") as pool_job:
@@ -623,7 +780,7 @@ The `Pool` object can be used to create a pool of `PoolJob` objects to run queri
623
780
 
624
781
  ```python
625
782
  import asyncio
626
- from mapepire_python.pool.pool_client import Pool, PoolOptions
783
+ from mapepire_python import Pool, PoolOptions
627
784
 
628
785
  async def main():
629
786
  async with Pool(
@@ -688,18 +845,57 @@ with connect("./mapepire.ini") as conn:
688
845
 
689
846
  ## PEP 249 Asynchronous Implementation
690
847
 
691
- 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:
848
+ 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`:
692
849
 
693
850
  ```python
694
851
  import asyncio
695
- from mapepire_python.asycnio import connect
852
+ from mapepire_python import async_connect
696
853
 
697
854
  async def main():
698
- async with connect("./mapepire.ini") as conn:
855
+ async with async_connect("./mapepire.ini") as conn:
699
856
  async with await conn.execute("select * from sample.employee") as cursor:
700
857
  result = await cursor.fetchone()
701
858
  print(result)
702
-
859
+
860
+ if __name__ == '__main__':
861
+ asyncio.run(main())
862
+ ```
863
+
864
+ Fetch multiple rows at once with `fetchmany` or drain the full result set with `fetchall`:
865
+
866
+ ```python
867
+ async def main():
868
+ async with async_connect("./mapepire.ini") as conn:
869
+ async with await conn.execute("select * from sample.employee") as cursor:
870
+ page = await cursor.fetchmany(10) # first 10 rows
871
+ rest = await cursor.fetchall() # remaining rows
872
+
873
+ if __name__ == '__main__':
874
+ asyncio.run(main())
875
+ ```
876
+
877
+ Stream rows one at a time using `async for`:
878
+
879
+ ```python
880
+ async def main():
881
+ async with async_connect("./mapepire.ini") as conn:
882
+ async for row in await conn.execute("select * from sample.employee"):
883
+ print(row)
884
+
885
+ if __name__ == '__main__':
886
+ asyncio.run(main())
887
+ ```
888
+
889
+ Use `AsyncSQLJob` directly for lower-level access without a pool:
890
+
891
+ ```python
892
+ from mapepire_python.client.async_sql_job import AsyncSQLJob
893
+
894
+ async def main():
895
+ async with AsyncSQLJob("./mapepire.ini") as job:
896
+ result = await job.query_and_run("select * from sample.employee")
897
+ print(result.data)
898
+
703
899
  if __name__ == '__main__':
704
900
  asyncio.run(main())
705
901
  ```
@@ -857,9 +1053,26 @@ conda deactivate
857
1053
  ```
858
1054
  ## Run local test suite
859
1055
 
860
- First, create a `pytest.ini` file in the `tests` directory.
1056
+ The suite is split in two:
1057
+
1058
+ - `tests/unit/` — offline unit tests. They mock the WebSocket transport and need
1059
+ **no** IBM i server or credentials, so they run anywhere (including CI on forks).
1060
+ - `tests/integration/` — live-server tests. They connect to a real Mapepire
1061
+ server and require credentials.
1062
+
1063
+ ### Unit tests (no server required)
1064
+
1065
+ ```bash
1066
+ # activate python development environment first
1067
+
1068
+ pytest tests/unit/
1069
+ ```
1070
+
1071
+ ### Integration tests (server required)
1072
+
1073
+ First, create a `pytest.ini` file in the `tests/integration` directory:
861
1074
 
862
- `tests/pytest.ini`
1075
+ `tests/integration/pytest.ini`
863
1076
 
864
1077
  ```ini
865
1078
  [pytest]
@@ -869,11 +1082,17 @@ env =
869
1082
  VITE_DB_PASS=PASS
870
1083
  ```
871
1084
 
872
- Run the test suite from the `mapepire-python` directory:
1085
+ Then run the integration suite from the `mapepire-python` directory:
873
1086
 
874
1087
  ```bash
875
1088
  # activate python development environment first
876
1089
 
1090
+ pytest tests/integration/
1091
+ ```
1092
+
1093
+ To run everything (unit + integration), just point pytest at `tests/`:
1094
+
1095
+ ```bash
877
1096
  pytest tests/
878
1097
  ```
879
1098