Experimentování s API

Ahoj,

spolu s @voyczech zkoušíme vytvořit program na zobrazení a sčítání statistik z Karmen. Po chvíli samostatného zkoušení a neúspěšných výsledků jsme postupovali podle tohoto příspěvku How to access data from the Karmen REST API with `curl`?.

curl -k -H “Authorization: Token YOUR_TOKEN_HERE” https://backend.next.karmen.tech/api/2/users/me/groups/

Hned první problém na který jsme narazili, bylo vybrání správného API tokenu, je potřeba v pokročilém nastavení změnit token z nahrávání souborů (upload files) na čtení (read all).

https://backend.next.karmen.tech/api/2/groups/<group_id>/print-jobs/

URL jsme vzali z již zmíněného postu, přičemž jsme použili tu, která nás vedla na print-jobs. ID skupiny jsme vzali z Karmen (Settings → Your Account → Octoprint file upload URL).

Poté zbývalo jen vložit API Token a vše mělo fungovat, bohužel jsme smazali z příkazu “Authorization: Token” i když tato část má zůstat a smazáno má být pouze YOUR_TOKEN_HERE, což nám došlo až později.

Momentálně vše funguje a jsme schopni dostat potřebná data z Karmen, abychom s nimi byli schopni následně pracovat. O čemž vás samozřejmě budu informovat tady ve fóru pokračováním tohoto postu.

Patrik

1 Like

Díky @patrik, že sdílíš svoje zkušenosti a progres :slight_smile:
Chybami se člověk i dle mě nejvíce naučí a jsem rád, že sdílíš i trable.

Je pravda, že když jsem viděl

curl -k -H “Authorization: Token YOUR_TOKEN_HERE nahrezeno API klíčem” https://backend.next.karmen.tech/api/2/users/me/groups/

Tak jsem veděl, že tak chybí část, ale taky jsem to opravil až na druhý pokus :sweat_smile: člověk se zkrátka stále učí novým věcem.

curl -k -H “Authorization: Token YOUR_TOKEN_HERE nahrezeno API klíčem” https://backend.next.karmen.tech/api/2/users/me/groups/

Jsem zvědav na tvůj progres a budu rád když sem budeš házet své “krůčky” :raised_hands:

Nové poznatky
Po opětovném otestování API jsem zjistil, že jsem schopen dostat data z Karmen, bohužel ale v endpointu print-jobs, který jsem použil nebyly data které jsem potřeboval, tedy především hmotnost použitého filamentu. Po vyzkoušení všech ostatním endpointů a přečtení dat z nich jsem zjistil, že data která potřebuji jsou ve files.

Nutno říct, že toto není úplně ideální, nejen protože ve výsledku nebudou přesně započítané nevydařené výtisky, také budu muset později implementovat verifikaci počtu výtisků jednoho souboru a také v některých souborech vznikají chyby pokud jsou data špatně zapsaná ze sliceru, na tento poslední problém se mi ještě nepovedlo najít řešení.

Každopádně se mi povedlo použit API souborů a dostat list potřebných dat, které jsem poté omezil na zobrazení pouze názvu, datumu a hmotnosti filamentu. Datum jsem poté zformátoval do evropského a snadně čitelného standardu.

Po ověření s databází v Karmen mi došlo že souborů, které mi můj kód vypsal je velmi málo. Příčinnou bylo stránkování, což jsem vyřešil opakováním curl příkazu dokud neprošel všechny stránky. Což velmi razatně zpomalilo celý program, přičemž momentálně jeden výpis může trvat i několik desítek sekund, což je také věc na kterou jsem zatím nenašel řešení.

Povedlo se mi ale udělat program použitelnější tím, že po spuštění vždy dotáže uživatele jak daleko do minulosti má být výpis dlouhý a vypíše sečtené údaje za daný počet dní od současnosti. To ale také nefunguje u všeho a také je to můj momentálně největší problém a to je, že program vždy sečte hmotnost filamentu z celé databáze i když byl dotázán na určitý počet dní. Například zeptáte se na údaje za posledních deset dní a program napíše kolik dní jste zadali, kolik souborů bylo uploadnuto (za daný čas) a hmotnost filamentu za celkovou databázi.

Příště se budu věnovat hlavně vyřešení problémů, které právě nastaly a pak se budu věnovat připojení druhého endpointu (print-jobs), který bude verifikovat počet tisků z každého souboru.

Na stránkování jsem narazil také při prvním osahání API, ale nějak jsem si už nenašel čas na další posezení u toho a případné vyřešení.

Prosím @patrik hodíš sem, jak jsi vyřešil stránkování? kód, skript atd
Díky :pray:

jasně @voyczech , nejdřív si zjistím kolik stránek je abych je neprocházel do nekonečna

def get_total_pages():
    json_data = get_files(1)
    if json_data and 'count' in json_data:
        total_count = json_data['count']
        items_per_page = json_data.get('per_page', 20) 
        return (total_count + items_per_page - 1) // items_per_page
    return 0

pak si pomocí while funkce projdu všechny stránky

 while total_pages > 0:
        json_data = get_files(total_pages)
        
        if not json_data or 'results' not in json_data:
            total_pages -= 1
            continue
        
        files = json_data.get('results', [])
        
        if not files:
            total_pages -= 1
            continue

bohužel se prochází od nejstaršího, takže když potřebuju zjistit soubory k aktuálnímu datu musím nechat API poslat všechny data a to chvíli trvá.

Patrik

Sčítání hmotnosti použitého filamentu se mi podařilo opravit tím, že jsem si vytvořil proměnnou souborů, které odpovídaly datu, které jsem zadal a poté s nimi provedl součet hmotností. K tomu jsem použil už funkci, kterou jsem používal již dříve.

def calculate_total_filament(data):
    total = sum(item.get('filament_used_g', 0) for item in data)
    return total

Předtím jsem tedy nepoužíval hodnotu calculate_total_filament a tak program počítal se všemi soubory.


 filtered_files = [file for file in files if 'uploadedOn' in file and
                          datetime.strptime(file['uploadedOn'], "%Y-%m-%dT%H:%M:%S.%fZ") >= cutoff_date]
total_material_used += calculate_total_filament(filtered_files)

Po přidání těchto tří řádků už vše běželo tak jak tomu bylo původně zamýšleno :slightly_smiling_face:

Implementace druhého API
K tomu abych byl schopen spočítat všechny vytištěné soubory je potřeba použít druhé API (print-jobs), které ale neobsahuje hmotnost filamentu.

Mojí strategií tedy bylo vzít tisky za danou dobu a přidat k nim přes file-id, které sdílí soubor s tiskem hmotnost filamentu, případně i další informace a poté je sečíst a sdělit uživateli.

Sice na tom stále pracuji, ale již se mi podařilo narazit na pár problémů, po napsání prvního kódu a spuštění programu se nezobrazovali žádné tisky ani po upravení zadané doby. Napadlo mě, že se nejspíše špatně čte file-id, protože kromě relevantních informací vše fungovalo správně. Zobrazil jsem si tedy data z API souborů a zjistil jsem, že každý soubor obsahuje hned několik různých id, každé v jiném listu.

Poté jsem se podíval i do json souboru z API tisků a přišel jsem na to, že v něm je sice napsané “file_id” jenže v souborech je pouze “id”, implementoval jsem tedy funkci, která tuto situaci řeší, bohužel to celkovému problému ale nepomohlo.

def match_and_rename_file_id(print_jobs, files):
    matched_print_jobs = []
    
    for print_job in print_jobs:
        file_id_in_job = print_job.get('file_id')
        
        for file in files:
            file_id_in_file = file.get('id')
            
            if file_id_in_job == file_id_in_file:
                # Rename the IDs as 'file_is' in both matched entries
                print_job['file_is'] = file_id_in_job
                file['file_is'] = file_id_in_file
                matched_print_jobs.append((print_job, file))
                break
    
    return matched_print_jobs

Neustále se mi zobrazovalo 0g využito a 0 souborů vytisknuto i za dlouhou dobu. Rozhodl jsem se tedy rozebrat jednotlivé funkce, abych přišel na to v které je problém, ukázalo se že získávání dat z print-jobs API se mi nepovedlo udělat správně a můj program selhává při získávání časových údajů, i poté co jsem se snažil získat pouhý list časů kdy byly jednotlivé výtisky spuštěny.

Patrik

Dokončeno! :slightly_smiling_face:

Povedlo se mi sestavit program, který počítá hmotnost využitého filamentu za daný časový úsek.

Jak to funguje?
Po spuštění python kódu se Vás program zeptá z kolika dní z minulosti by jste chtěli data zobrazit. Po zadání odešle žádost do Vašeho Karmen API získá potřebné informace a během cca 45 sekund Vám zobrazí sečtený počet vytištěných nesmazaných souborů a kolik filamentu spotřebovaly.

Před spuštěním kódu je potřeba do něj vložit API klíč a Vaše group_id.

import subprocess
import json
from datetime import datetime, timedelta

def get_print_jobs(page):
    curl_command = [
        "curl", "-k", "-H", "Authorization: Token <api_key>", 
        f"https://backend.next.karmen.tech/api/2/groups/<group_id>/print-jobs/?page={page}"
    ]
    result = subprocess.run(curl_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    
    if result.returncode != 0:
        return None
    
    return json.loads(result.stdout.decode())

def get_files(page):
    curl_command = [
        "curl", "-k", "-H", "Authorization: Token <api_key>", 
        f"https://backend.next.karmen.tech/api/2/groups/<group_id>/files/?page={page}"
    ]
    result = subprocess.run(curl_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    
    if result.returncode != 0:
        return None
    
    return json.loads(result.stdout.decode())

def get_print_jobs_within_days(days_past):
    id_file_started_on_list = []
    page = 1
    cutoff_date = datetime.now() - timedelta(days=days_past)

    while True:
        print_jobs_data = get_print_jobs(page)
        
        if not print_jobs_data or 'results' not in print_jobs_data:
            break
        
        print_jobs = print_jobs_data['results']
        
        for print_job in print_jobs:
            job_id = print_job.get('id', None)
            started_on = print_job.get('started_on', None)
            file_id = print_job.get('file_id', None) 
            
            if job_id and started_on and file_id:
                job_started_on = datetime.strptime(started_on, "%Y-%m-%dT%H:%M:%S.%fZ")
                
                if job_started_on < cutoff_date:
                    return id_file_started_on_list
                
                id_file_started_on_list.append({'id': job_id, 'file_id': file_id, 'started_on': started_on})
        
        if print_jobs_data.get('next'):
            page += 1
        else:
            break
    
    return id_file_started_on_list

def get_filament_weight_for_files(print_jobs):
    total_filament_used = 0
    page = 1
    file_id_to_filament = {}

    while True:
        files_data = get_files(page)
        
        if not files_data or 'results' not in files_data:
            break
        
        files = files_data['results']
        
        for file in files:
            file_id = file.get('id')
            filament_used = file.get('filament_used_g', 0)
            if file_id:
                file_id_to_filament[file_id] = filament_used
        
        if files_data.get('next'):
            page += 1
        else:
            break
    
    for print_job in print_jobs:
        file_id = print_job['file_id']
        if file_id in file_id_to_filament:
            total_filament_used += file_id_to_filament[file_id]
    
    return total_filament_used

if __name__ == "__main__":
    days = int(input("Enter the number of days to filter print jobs from: "))
    
    print_jobs = get_print_jobs_within_days(days)
    
    total_filament_used = get_filament_weight_for_files(print_jobs)
    
    print(f"Total print jobs in the past {days} days: {len(print_jobs)}")
    print(f"Total filament used in the past {days} days: {round(total_filament_used)}g")

Problém z posledního postu a tím i další menší problémy, které zbývaly, jsem vyřešil tím, že jsem předělal celou strukturu.
Začal jsem tedy tím, že jsem vytvořil funkce, která získávala časy a file_id z print-jobs a z nich rovnou sestavila list tisků, které se staly za dobu danou uživatelem. Také jsem otočil procházení stránek a zavedl jsem zastavení poté co se dostane za časový horizont, čímž se celý proces velmi zrychlil.
Až po těchto všech úkonech se přiřadí hmotnosti filamentu z files pomocí shodných id. Počítá se s tím, že soubory mohou být tisknuty víckrát a proto se id může přiřadit i k více tiskům. Následně se vše sečte a zobrazí se výsledná data uživateli.

Patrik

1 Like