mapepire-python 0.2.0__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 (46) hide show
  1. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/PKG-INFO +255 -42
  2. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/README.md +242 -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.2.0 → mapepire_python-0.3.0}/mapepire_python/asyncio/__init__.py +12 -3
  6. {mapepire_python-0.2.0 → 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.2.0 → mapepire_python-0.3.0}/mapepire_python/base_job.py +6 -4
  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.2.0 → mapepire_python-0.3.0}/mapepire_python/client/sql_job.py +31 -18
  13. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/core/connection.py +7 -2
  14. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/core/cursor.py +48 -21
  15. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/core/exceptions.py +18 -20
  16. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/core/utils.py +38 -9
  17. mapepire_python-0.3.0/mapepire_python/data_types.py +528 -0
  18. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/pool/pool_client.py +42 -14
  19. mapepire_python-0.3.0/mapepire_python/pool/pool_job.py +36 -0
  20. mapepire_python-0.3.0/mapepire_python/pool/pool_query.py +168 -0
  21. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/query_manager.py +0 -3
  22. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/ssl.py +1 -0
  23. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/version.py +1 -1
  24. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/websocket.py +16 -3
  25. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python.egg-info/PKG-INFO +255 -42
  26. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python.egg-info/SOURCES.txt +3 -1
  27. mapepire_python-0.3.0/mapepire_python.egg-info/requires.txt +25 -0
  28. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/pyproject.toml +44 -17
  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.0}/LICENSE +0 -0
  38. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/client/__init__.py +0 -0
  39. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/client/websocket_client.py +0 -0
  40. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/core/__init__.py +0 -0
  41. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/pool/__init__.py +0 -0
  42. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/pool/async_websocket_client.py +0 -0
  43. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python/py.typed +0 -0
  44. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python.egg-info/dependency_links.txt +0 -0
  45. {mapepire_python-0.2.0 → mapepire_python-0.3.0}/mapepire_python.egg-info/top_level.txt +0 -0
  46. {mapepire_python-0.2.0 → 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.2.0
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,41 +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
220
  Requires-Dist: websockets>=14.0
220
- Requires-Dist: pyee
221
221
  Requires-Dist: pep249abc
222
222
  Provides-Extra: dev
223
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"
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"
229
228
  Requires-Dist: pep249abc; extra == "dev"
230
229
  Requires-Dist: pytest-asyncio; extra == "dev"
231
- Requires-Dist: pytest-sphinx; extra == "dev"
232
230
  Requires-Dist: pytest-cov; extra == "dev"
233
231
  Requires-Dist: twine>=1.11.0; extra == "dev"
234
232
  Requires-Dist: build; extra == "dev"
235
233
  Requires-Dist: setuptools; extra == "dev"
236
234
  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
235
  Requires-Dist: packaging; extra == "dev"
244
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
245
242
 
246
243
  # mapepire-python
247
244
  <div align="center">
@@ -265,10 +262,16 @@ Requires-Dist: pre-commit; extra == "dev"
265
262
  - [Setup](#setup)
266
263
  - [Install with `pip`](#install-with-pip)
267
264
  - [Server Component Setup](#server-component-setup)
268
- - [Connection options](#connection-options)
265
+ - [Quick Start](#quick-start)
266
+ - [Other Connection options](#other-connection-options)
269
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)
270
271
  - [2. Passing the connection details as a dictionary](#2-passing-the-connection-details-as-a-dictionary)
271
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)
272
275
  - [Usage](#usage)
273
276
  - [1. Using the `SQLJob` object to run queries synchronously](#1-using-the-sqljob-object-to-run-queries-synchronously)
274
277
  - [Query and run](#query-and-run)
@@ -332,43 +335,124 @@ pip install mapepire-python
332
335
  ### Server Component Setup
333
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/)
334
337
 
335
-
336
- # Connection options
338
+ # Quick Start
337
339
 
338
- There are three ways to configure mapepire server connection details using `mapepire-python`:
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)
366
+
367
+ There are four ways to configure mapepire server connection details using `mapepire-python`:
339
368
 
340
369
  1. Using the `DaemonServer` object
341
370
  2. Passing the connection details as a dictionary
342
371
  3. Using a config file (`.ini`) to store the connection details
372
+ 4. Using environment variables
343
373
 
344
374
  ## 1. Using the `DaemonServer` object
345
375
 
346
- 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`:
347
377
 
348
378
  ```python
349
- from mapepire_python.data_types import DaemonServer
379
+ from mapepire_python import DaemonServer
350
380
 
351
381
  creds = DaemonServer(
352
382
  host="SERVER",
353
383
  port="PORT",
354
384
  user="USER",
355
- password="PASSWORD",
356
- ignoreUnauthorized=True
385
+ password="PASSWORD"
357
386
  )
358
387
  ```
359
388
 
360
389
  Once you have created the `DaemonServer` object, you can pass it to the `SQLJob` object to connect to the mapepire server:
361
390
 
362
391
  ```python
363
- from mapepire_python.client.sql_job import SQLJob
364
- from mapepire_python.data_types import DaemonServer
392
+ from mapepire_python import SQLJob, DaemonServer
365
393
 
366
394
  creds = DaemonServer(
367
395
  host="SERVER",
368
396
  port="PORT",
369
397
  user="USER",
370
- password="PASSWORD",
371
- ignoreUnauthorized=True
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
413
+
414
+ creds = DaemonServer(
415
+ host="SERVER",
416
+ password=KerberosTokenProvider(host="SERVER"),
417
+ user="USER",
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,
454
+ user="USER",
455
+ port="PORT",
372
456
  )
373
457
 
374
458
  job = SQLJob(creds)
@@ -379,7 +463,7 @@ job = SQLJob(creds)
379
463
  You can also use a dictionary to configure the connection details:
380
464
 
381
465
  ```python
382
- from mapepire_python.client.sql_job import SQLJob
466
+ from mapepire_python import SQLJob
383
467
 
384
468
  creds = {
385
469
  "host": "SERVER",
@@ -412,13 +496,80 @@ Then you can create a `SQLJob` object by passing the path to the `.ini` file whi
412
496
 
413
497
 
414
498
  ```python
415
- from mapepire_python.client.sql_job import SQLJob
499
+ from mapepire_python import SQLJob
416
500
 
417
501
  job = SQLJob("./mapepire.ini", section="mapepire")
418
502
  ```
419
503
 
420
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.
421
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
+
422
573
 
423
574
  # Usage
424
575
 
@@ -436,7 +587,7 @@ There are four main ways to run queries using `mapepire-python`:
436
587
  ## 1. Using the `SQLJob` object to run queries synchronously
437
588
 
438
589
  ```python
439
- from mapepire_python.client.sql_job import SQLJob
590
+ from mapepire_python import SQLJob
440
591
 
441
592
  with SQLJob("./mapepire.ini") as sql_job:
442
593
  with sql_job.query("select * from sample.employee") as query:
@@ -580,7 +731,7 @@ In the ouput above, the query was successful and returned one row of data.
580
731
  To create and run a query in a single step, use the `query_and_run` method:
581
732
 
582
733
  ```python
583
- from mapepire_python.client.sql_job import SQLJob
734
+ from mapepire_python import SQLJob
584
735
 
585
736
  with SQLJob("./mapepire.ini") as sql_job:
586
737
  # query automatically closed after running
@@ -594,7 +745,7 @@ The `PoolJob` object can be used to create and run queries asynchronously:
594
745
 
595
746
  ```python
596
747
  import asyncio
597
- from mapepire_python.pool.pool_job import PoolJob
748
+ from mapepire_python import PoolJob
598
749
 
599
750
  async def main():
600
751
  async with PoolJob("./mapepire.ini") as pool_job:
@@ -610,7 +761,7 @@ To run a create and run a query asynchronously in a single step, use the `query_
610
761
 
611
762
  ```python
612
763
  import asyncio
613
- from mapepire_python.pool.pool_job import PoolJob
764
+ from mapepire_python import PoolJob
614
765
 
615
766
  async def main():
616
767
  async with PoolJob("./mapepire.ini") as pool_job:
@@ -629,7 +780,7 @@ The `Pool` object can be used to create a pool of `PoolJob` objects to run queri
629
780
 
630
781
  ```python
631
782
  import asyncio
632
- from mapepire_python.pool.pool_client import Pool, PoolOptions
783
+ from mapepire_python import Pool, PoolOptions
633
784
 
634
785
  async def main():
635
786
  async with Pool(
@@ -694,18 +845,57 @@ with connect("./mapepire.ini") as conn:
694
845
 
695
846
  ## PEP 249 Asynchronous Implementation
696
847
 
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:
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`:
698
849
 
699
850
  ```python
700
851
  import asyncio
701
- from mapepire_python.asycnio import connect
852
+ from mapepire_python import async_connect
702
853
 
703
854
  async def main():
704
- async with connect("./mapepire.ini") as conn:
855
+ async with async_connect("./mapepire.ini") as conn:
705
856
  async with await conn.execute("select * from sample.employee") as cursor:
706
857
  result = await cursor.fetchone()
707
858
  print(result)
708
-
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
+
709
899
  if __name__ == '__main__':
710
900
  asyncio.run(main())
711
901
  ```
@@ -863,9 +1053,26 @@ conda deactivate
863
1053
  ```
864
1054
  ## Run local test suite
865
1055
 
866
- 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:
867
1074
 
868
- `tests/pytest.ini`
1075
+ `tests/integration/pytest.ini`
869
1076
 
870
1077
  ```ini
871
1078
  [pytest]
@@ -875,11 +1082,17 @@ env =
875
1082
  VITE_DB_PASS=PASS
876
1083
  ```
877
1084
 
878
- Run the test suite from the `mapepire-python` directory:
1085
+ Then run the integration suite from the `mapepire-python` directory:
879
1086
 
880
1087
  ```bash
881
1088
  # activate python development environment first
882
1089
 
1090
+ pytest tests/integration/
1091
+ ```
1092
+
1093
+ To run everything (unit + integration), just point pytest at `tests/`:
1094
+
1095
+ ```bash
883
1096
  pytest tests/
884
1097
  ```
885
1098