Metadata-Version: 2.4
Name: py_gazie
Version: 0.1.7
Summary: A Seamless Bridge Between Custom Python Applications and Easy GAzie Database Consultation
Home-page: https://www.newstechnology.eu
Author: Daniele Frulla
Author-email: daniele.frulla@newstechnology.eu
Keywords: python,GAzie,ERP,Gestione Aziendale,py-gazie,gazie,gazie database,php-crud-api
Description-Content-Type: text/markdown
Requires-Dist: requests==2.34.2
Requires-Dist: py_client_cap
Dynamic: author
Dynamic: author-email
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: requires-dist
Dynamic: summary

# Libreria Python Consultazione DB GAzie

Client Python per consultare (e aggiornare) il DB **GAzie** tramite php-crud-api.

Questa pagina è la guida d'uso completa.

## Installazione

```bash
pip install py_gazie
```

## Uso rapido

```python
from py_gazie import GazieClient

with GazieClient("http://127.0.0.1:8082/modules/api/index.php", "test-token",
                 timeout=15, default_company="001") as client:

    product = client.get_table("product")       # alias -> ProductTable (gaz_001artico)
    resp = product.page(limit=5).search()       # chaining: prima i filtri/pagina, poi search()

    if resp.success:                            # controlla SEMPRE success
        for rec in resp.data.records:           # NavigableRecord: attributi, non .get()
            print(rec.codice, "-", rec.descri)
        print("totale:", resp.data.pagination.total_records)
    else:
        print("errore:", resp.status_code, resp.error)
```

## Le tabelle: cosa passare a `get_table()`

| Vuoi | Passa | Ricevi | Sul wire |
|---|---|---|---|
| clienti | `"customer"` | `CustomerTable` | `gaz_{company}clfoco` |
| articoli | `"product"` | `ProductTable` | `gaz_{company}artico` |
| anagrafiche fiscali | `"anagra"` | `AnagraTable` | `gaz_anagra` |
| aziende | `"company"` | `CompanyTable` | `gaz_aziend` |
| documenti di fatturazione (testata) | `"document"` | `DocumentTable` | `gaz_{company}tesdoc` |
| altre tabelle | nome reale, es. `"gaz_001catmer"` | `GazieTable` generica | stesso nome |

- **Solo gli alias attivano i metodi di dominio** (`insert_record`, `get_by_code`,
  `search_by_description`, `get_rows`, `get_customer`…): il nome reale dà sempre la tabella generica.
- Tabella inesistente (o `company` non presente sul server) → **`ValueError`** con il nome della
  tabella mancante.
- Le tabelle sono **cachate**: stessa `company`+nome → stessa istanza. La `company` esplicita vince
  su `default_company`; entrambe sono normalizzate a 3 cifre (`2` → `'002'`).

## Classe `GazieTable` — operazioni

**Tutte le tabelle** (alias e generiche) hanno lo stesso nucleo: `create` · `update` · `delete` · `get_by_id` · `search` ·
`first` · `count` · `exists` · `add_filter` · `order_by`. Schema del chaining:

```python
t = client.get_table("customer")
t.and_filter("codice", "sw", "103")     # AND (ripeti per aggiungere)
t.or_filter("descri", "cs", "Fermi")    # OR (filter1/filter2)
t.add_filter("or", "descri,cs,Fermi")   # stessa cosa da stringa 'campo,op,valore'
t.page(1, 10).order_by("codice,desc")   # pagina + ordinamento (order_by = alias di order)
resp = t.search()                       # -> ApiResponse

resp = t.read()                         # lista | t.read(item_id) -> record singolo
resp = t.get_by_id(42)                  # UN record per chiave primaria -> ApiResponse
resp = t.first()                        # primo record o None
n    = t.count()                        # totale filtrato (misurato:14 su artico)
ok   = t.exists()                       # almeno uno?
resp = t.create({...}) / t.update(id, {...}) / t.delete(id)
```

⚠️ I filtri si **accumulano sull'istanza** e vengono azzerati solo quando leggi: mai lasciare filtri
appesi senza chiamare `search()`/`read()`.

### Operatori di filtro (misurati sul server, 22/09/2026)

- ✅ `eq`, `neq`, `gt`, `sw` (inizia per), `cs` (contiene), `in` (valori separati da `,`)
- ❌ **`lk` viene ignorato dal server** (restituisce tutto): usa `cs`
- AND = `filter` ripetuto · OR = `filter1`/`filter2` · misto = intersezione
- niente virgole nei valori: il filtro è la stringa `"campo,op,valore"`

## Formato della risposta

Ogni operazione ritorna un `ApiResponse` con quattro campi: **`success`**, **`status_code`**,
**`data`**, **`error`**.

| Cosa manda il server | Come si legge in `data` |
|---|---|
| elenco `{"records": [...], "results": N}` | `PaginatedData`: `resp.data.records` (lista), `len(resp.data)`, `resp.data.pagination.total_records` |
| record singolo | dict grezzo: `resp.data["codice"]` |
| errore (es. `404`, code `1003`) | `success=False`, `error="Record … not found"` |
| warning PHP (colonna inesistente) | ⚠️ `success=True` ma `data` è una **stringa HTML**: controlla il tipo |

I record dei elenchi sono **`NavigableRecord`**: accesso per attributo (`rec.codice`,
`rec.indirizzo.citta` per i campi annidati). **Non esiste** `rec.get("codice")`.

```python
resp = client.get_table("product").page(limit=5).search()
if resp.success and hasattr(resp.data, "records"):
    for rec in resp.data.records:
        print(rec.codice, rec.descri)
```

## Cinque regole d'oro

1. **Controlla sempre `resp.success`** — gli errori HTTP/rete diventano `success=False`, niente eccezioni.
2. Record dagli elenchi → **attributi** (`rec.campo`), mai `.get()`.
3. I metodi dichiarati `-> List[…]` in realtà ritornano `ApiResponse`: leggi `resp.data.records`.
4. **Mai `lk`** nei filtri (ignorato dal server): `cs` = contiene, `eq` = uguale.
5. `create()` completa **da solo** il payload: legge lo schema `create-{t}` da `/openapi` e
   valorizza tutte le colonne tranne `id` (autoincrement, tornato come `resp.data`) — tu passi
   solo quelle che ti servono. Puoi passare `datetime.date`/`datetime`/`Decimal`: vengono
   convertiti in stringhe (formato server `YYYY-MM-DD HH:MM:SS`), perché `json.dumps` non
   accetta oggetti. `company` non è una colonna: non va nel payload.
6. Su `customer` il **`codice` lo calcola la libreria** (progressivo `mascli` di `gaz_aziend`):
   `create()` e `insert_record()` ignorano il codice che passi e generano il prossimo del
   mastro (es. `103000009`), riprovando se il server risponde `409 Duplicate key`.

## Adattare GAzie a CRUD PHP API

Per adattare il database GAzie a php-crud-api occorre inserire il campo `id` come chiave primaria
ove non vi sia (php-crud-api lo richiede per gli URL `/records/{tabella}/{id}`).

### Procedura da eseguire sul DB GAzie

```sql
DELIMITER //

DROP PROCEDURE IF EXISTS AddIdPrimaryKeyToGazie //

CREATE PROCEDURE AddIdPrimaryKeyToGazie()
BEGIN
    -- =========================================================================
    -- 1. TUTTE LE DICHIARAZIONI (Devono stare in cima)
    -- =========================================================================
    DECLARE done INT DEFAULT FALSE;
    DECLARE current_table VARCHAR(255);
    DECLARE old_pk_columns VARCHAR(512);
    
    -- Cursore per escludere tabelle con 'id' o con qualsiasi altro AUTO_INCREMENT
    DECLARE table_cursor CURSOR FOR 
        SELECT t.TABLE_NAME 
        FROM information_schema.TABLES t
        WHERE t.TABLE_SCHEMA = DATABASE()
          AND t.TABLE_NAME LIKE 'gaz_%'
          AND NOT EXISTS (
              SELECT 1 
              FROM information_schema.COLUMNS c 
              WHERE c.TABLE_SCHEMA = DATABASE() 
                AND c.TABLE_NAME = t.TABLE_NAME 
                AND c.COLUMN_NAME = 'id'
          )
          AND NOT EXISTS (
              SELECT 1 
              FROM information_schema.COLUMNS c 
              WHERE c.TABLE_SCHEMA = DATABASE() 
                AND c.TABLE_NAME = t.TABLE_NAME 
                AND c.EXTRA LIKE '%auto_increment%'
          );

    -- L'handler deve essere l'ultima cosa dichiarata
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;


    -- =========================================================================
    -- 2. ISTRUZIONI ESEGUIBILI
    -- =========================================================================
    
    -- Creazione della tabella temporanea per il LOG
    DROP TEMPORARY TABLE IF EXISTS temp_log_alter_tables;
    CREATE TEMPORARY TABLE temp_log_alter_tables (
        id INT AUTO_INCREMENT PRIMARY KEY,
        eseguito_alle TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        tabella_modificata VARCHAR(255),
        query_eseguita TEXT
    );

    OPEN table_cursor;

    read_loop: LOOP
        FETCH table_cursor INTO current_table;
        
        IF done THEN
            LEAVE read_loop;
        END IF;

        -- Recupera dinamicamente le colonne che compongono l'attuale chiave primaria
        SELECT GROUP_CONCAT(COLUMN_NAME SEPARATOR ', ')
        INTO old_pk_columns
        FROM information_schema.KEY_COLUMN_USAGE
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = current_table
          AND CONSTRAINT_NAME = 'PRIMARY';

        -- Se esiste una vecchia chiave primaria, procediamo
        IF old_pk_columns IS NOT NULL THEN
            
            SET @dynamic_sql = CONCAT(
                'ALTER TABLE `', current_table, '` ',
                'DROP PRIMARY KEY, ',
                'ADD COLUMN `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST, ',
                'ADD UNIQUE KEY `uk_', current_table, '_old_pk` (', old_pk_columns, ');'
            );

            -- Scrittura nel LOG
            INSERT INTO temp_log_alter_tables (tabella_modificata, query_eseguita) 
            VALUES (current_table, @dynamic_sql);

            -- Esecuzione della query
            PREPARE stmt FROM @dynamic_sql;
            EXECUTE stmt;
            DEALLOCATE PREPARE stmt;
            
        END IF;

    END LOOP;

    CLOSE table_cursor;
    
    -- 3. Stampa a schermo del risultato finale
    SELECT * FROM temp_log_alter_tables;
    
    -- Pulizia finale
    DROP TEMPORARY TABLE IF EXISTS temp_log_alter_tables;

END //

DELIMITER ;
```

### Chiamare la procedura

```sql
CALL AddIdPrimaryKeyToGazie();
```
