"""Docházkový terminál v příkazové řádce – příklad klienta Profirest API v2 pro Python. Spuštění: python pichacka.py Proměnné: PROFIREST_BASE_URL (výchozí https://api.mujprofirest.cz), PROFIREST_TOKENS (soubor s tokeny, výchozí ./profirest-pichacka.json) Příkazy: p PIN = příchod, o PIN = odchod, pz PIN = začátek přestávky, kz PIN = konec přestávky, k = kdo je v práci, odparovat = zapomenout spárování, q = konec """ import os import sys try: from profirest_api import ApiError, FileStorage, OAuthError, ProfirestApi, uuid7 except ImportError: # spuštěno ze zdrojů SDK (python/examples) bez instalace balíčku sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'src')) from profirest_api import ApiError, FileStorage, OAuthError, ProfirestApi, uuid7 # české znaky i v konzoli Windows a při přesměrování do souboru sys.stdout.reconfigure(encoding='utf-8') sys.stdin.reconfigure(encoding='utf-8') SCOPE = 'restaurants:read staff:read staff:authenticate attendance:read attendance:punch' COMMANDS = {'p': 'clock_in', 'o': 'clock_out', 'pz': 'break_start', 'kz': 'break_end'} STATES = {'working': 'v práci', 'on_break': 'na přestávce', 'off': 'mimo práci'} HELP = ('Příkazy: p PIN = příchod, o PIN = odchod, pz PIN = začátek přestávky, kz PIN = konec přestávky,\n' ' k = kdo je v práci, odparovat = zapomenout spárování, q = konec') def pair(api): """Spáruje terminál kódem: vedoucí ho potvrdí v administraci (Správa PC / zařízení → Připojit zařízení).""" pairing = api.pair(scope=SCOPE, device_name='Píchačka (příklad)') print('KÓD: ' + pairing.user_code, flush=True) print('Potvrďte ho v administraci, nebo otevřete ' + str(pairing.verification_uri_complete), flush=True) try: pairing.wait() # blokuje, dokud vedoucí kód nepotvrdí (ptá se po intervalu serveru, 5 s) except OAuthError as e: if e.error == 'access_denied': print('Vedoucí párování odmítl.') elif e.error == 'expired_token': print('Kód vypršel (10 minut bez potvrzení). Spusťte program znovu.') else: print('Párování selhalo: ' + e.error) sys.exit(1) def punch(api, kind, pin): """Jedno píchnutí. id vznikne jednou – i opakované odeslání (výběr zaměstnance) jde se stejným id.""" body = {'id': uuid7(), 'type': kind, 'credential': {'method': 'pin', 'secret': pin}} path = '/restaurants/' + api.restaurant_id + '/attendance/punches' while True: try: r = api.post(path, body) print('OK: %s – %s' % (r['employee']['name'], STATES.get(r['state'], r['state'])), flush=True) return except ApiError as e: if e.code == 'staff_ambiguous': # stejný PIN má víc lidí: nechat vybrat a poslat znovu s employee_id print('Tento PIN má víc zaměstnanců:') for i, c in enumerate(e.candidates, 1): print(' %d) %s' % (i, c['name'])) print('Číslo:', end=' ', flush=True) choice = sys.stdin.readline().strip() if not choice.isdigit() or not 1 <= int(choice) <= len(e.candidates): print('Zrušeno.') return body['credential']['employee_id'] = e.candidates[int(choice) - 1]['id'] continue if e.code == 'staff_credentials_invalid': print('Neznámý PIN.') elif e.code == 'staff_locked': print('Příliš mnoho špatných PINů – zkuste to za %s s.' % (e.retry_after or 'chvíli')) elif e.code == 'attendance_state_conflict': print('Nejde to: ' + e.title) # např. odchod bez příchodu elif e.code in ('network_error', 'timeout'): print('Bez sítě – zkuste znovu.') # skutečný terminál by píchnutí uložil a poslal později else: print('Chyba: %s (%s)' % (e.title, e.code)) return def who_is_working(api): """Kdo je v práci: všechny stránky GET /restaurants/{id}/attendance/presence.""" people = api.all('/restaurants/' + api.restaurant_id + '/attendance/presence') if not people: print('Nikdo.') for p in people: print(' %s – %s' % (p['employee']['name'], STATES.get(p['state'], p['state']))) def main(): api = ProfirestApi( base_url=os.environ.get('PROFIREST_BASE_URL', 'https://api.mujprofirest.cz'), client_id='profirest-dochazka', # tokeny přežijí restart programu; soubor chraňte jako heslo storage=FileStorage(os.environ.get('PROFIREST_TOKENS', 'profirest-pichacka.json')), on_unpaired=lambda reason: print('Zařízení bylo odpárováno – spusťte program znovu.'), ) if not api.is_paired(): pair(api) if api.restaurant_id is None: api.connection() # GET /v2/connection zapamatuje provozovnu terminálu print('Provozovna ' + str(api.restaurant_id)) print(HELP, flush=True) for line in sys.stdin: # EOF (Ctrl+D / Ctrl+Z) = konec parts = line.split() if not parts: continue cmd = parts[0].lower() try: if cmd == 'q': break elif cmd == 'k': who_is_working(api) elif cmd == 'odparovat': api.unpair() # zapomene tokeny a refresh token zneplatní na serveru print('Odpárováno.') break elif cmd in COMMANDS and len(parts) == 2: punch(api, COMMANDS[cmd], parts[1]) else: print(HELP) except ApiError as e: if e.code == 'not_paired': print('Terminál není spárovaný – spusťte program znovu.') return 1 print('Chyba: %s (%s)' % (e.title, e.code)) sys.stdout.flush() return 0 if __name__ == '__main__': sys.exit(main())