#!/usr/bin/env python3 """Athena Deck prototype: loopback API, owned model processes, read-only telemetry.""" from video import Video from execution_setup import assess as execution_assess import argparse import os import hashlib import secrets from http.cookies import SimpleCookie, CookieError from auth import CredentialStore, verify_password from catalog import Catalog from tts_runtime import TTSRuntime from stt import STT,read_upload from audio_policy import AudioPolicy from tts_test import TTSTests from profiles import Profiles from capacity import assess, overview from runtime import Runtime from image_runtime import ImageRuntime from image_test import ImageTests from prompt_enhancer import PromptEnhancer from docker_support import DockerSupport from inference import LlamaWorker,Scheduler from endpoint import Endpoint from chat_test import ChatTests from auto_test import AutoTests from urllib.parse import urlsplit, parse_qs, unquote from network.client import NetworkClient from network.config import parse_config, ConfigError import json import signal import socket import subprocess import sys import threading import time import urllib.request from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path ROOT = Path(__file__).resolve().parent class HardwareProvider: def __init__(self): self.lock = threading.Lock() self.cached = None self.checked = 0 def snapshot(self): if os.environ.get('DECK_LOCAL_HARDWARE', '1') == '1': from collect_hardware import collect with self.lock: if self.cached is None or time.monotonic()-self.checked >= 1: self.cached = dict(collect(), available=True) self.checked = time.monotonic() return self.cached with self.lock: if time.monotonic() - self.checked < 1 and self.cached is not None: return self.cached try: result = subprocess.run(['ssh', '-i', str(Path.home()/'.ssh/athena_key'), '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=4', '-o', 'StrictHostKeyChecking=yes', 'root@192.168.1.212', 'python3 -'], input=(ROOT/'collect_hardware.py').read_text(), capture_output=True, text=True, timeout=10, check=True) self.cached = json.loads(result.stdout) self.cached['available'] = True except (OSError, subprocess.SubprocessError, ValueError): self.cached = dict(available=False, sampled_at=None, cpu={}, ram={}, gpus=[], errors=['Athena per SSH nicht erreichbar oder Messung fehlgeschlagen.']) self.checked = time.monotonic() return self.cached class Server(ThreadingHTTPServer): daemon_threads = True def __init__(self, port, state_dir=None): super().__init__((os.environ.get('DECK_BIND_HOST','127.0.0.1'), port), Handler) self.catalog = Catalog(Path(state_dir or os.environ.get("DECK_STATE_DIR", ROOT/".state"))/"models") self.runtime = Runtime(Path(state_dir or os.environ.get("DECK_STATE_DIR", ROOT/".state"))/"runtime") self.profiles = Profiles(self.catalog.root.parent/"profiles.json",self.catalog) self.runtime.catalog=self.catalog self.runtime.profiles=self.profiles self.image_tests = ImageTests(self.catalog.root.parent/"image-tests",self.profiles) self.image_runtime = ImageRuntime(self.catalog.root.parent/"image-runtime") self.image_tests.runtime = self.image_runtime self.prompt_enhancer=PromptEnhancer(self.catalog.root.parent/'prompt-enhancers',self.catalog,self.profiles,self.image_runtime) self.image_tests.enhancer=self.prompt_enhancer self.profiles.prompt_enhancer_ready=lambda task:bool(self.prompt_enhancer.installed(task)) self.profiles.image_runtime_ready=lambda:self.image_tests.status()["runtime_installed"] self.tts_runtime=TTSRuntime(self.catalog.root.parent/'tts-runtime') self.tts_tests=TTSTests(self.catalog.root.parent/'tts-tests',self.profiles,self.tts_runtime) self.profiles.tts_blockers=self.tts_tests.blockers self.docker = DockerSupport() self.hardware = HardwareProvider() self.started = time.time() self.network = NetworkClient() if os.environ.get('DECK_AUTH_RPC') == '1': from network.rpc import request self.credentials = CredentialStore(rpc=request) else: self.credentials = CredentialStore(Path(state_dir or os.environ.get('DECK_STATE_DIR', ROOT/'.state'))/'auth.json') if os.environ.get('DECK_REQUIRE_SETUP') == '1' and self.credentials.read() is None: self.server_close() raise RuntimeError('Serverzugang muss vor dem Start eingerichtet werden.') self.worker=LlamaWorker(self.catalog.root.parent/'llama-worker',self.catalog,self.runtime) self.scheduler=Scheduler(self.worker) self.profiles.chat_blockers=self.worker.blockers self.image_tests.acquire=self.scheduler.image_reservation self.prompt_enhancer.acquire=self.scheduler.image_reservation self.tts_tests.acquire=self.scheduler.tts_reservation self.scheduler.evict_tts=self.tts_tests.unload_idle self.stt=STT(self.profiles,self.catalog,self.runtime) self.profiles.stt_blockers=self.stt.blockers self.endpoint=Endpoint(self.catalog.root.parent,self.profiles,self.worker,self.scheduler,self.image_tests,self.credentials,self.server_port) self.endpoint.tts=self.tts_tests self.endpoint.stt=self.stt self.chat_tests=ChatTests(self.profiles,self.worker,self.scheduler) self.auto_tests=AutoTests(self.catalog.root.parent/'auto-tests.json',self.profiles,self.worker,self.scheduler) def stop_gpu_work(): self.music.stop();self.auto_tests.stop();self.chat_tests.stop();self.image_tests.stop();self.prompt_enhancer.stop_preview();self.tts_tests.stop();self.worker.stop() from video_original import VideoRuntimes from ltx_original_runtime import LTXOriginalRuntime from audio_cpp_runtime import AudioCppRuntime self.audio_cpp_runtime=AudioCppRuntime(self.catalog.root.parent/'audio-cpp-runtime') self.profiles.music_runtime_ready=lambda:self.audio_cpp_runtime.status()['installed'] from music import Music self.music=Music(self.catalog.root.parent/'music-jobs',self.profiles,self.catalog,self.audio_cpp_runtime,self.scheduler) self.profiles.music_blockers=self.music.blockers self.endpoint.music=self.music from profiles import audio_package_recipe self.profiles.runtime_recipe=lambda model:audio_package_recipe(model,self.audio_cpp_runtime.paths()[1]) self.ltx_original_runtime=LTXOriginalRuntime(self.catalog.root.parent/'ltx-original-runtime',self.image_runtime) self.video=VideoRuntimes(self.scheduler,stop_gpu_work,self.docker,self.catalog.root.parent/'video',self.catalog,self.profiles,self.image_runtime,original_runtime=self.ltx_original_runtime) self.endpoint.video=self.video self.profiles.video_runtime_ready=lambda:self.image_runtime.status().get("installed",False) from dashboard_data import Dashboard self.dashboard=Dashboard(self,self.catalog.root.parent) if self.endpoint.config['autostart']: try:self.endpoint.start() except ValueError as exc:self.endpoint.error=str(exc) self.audio_policy=AudioPolicy(self.catalog.root.parent/'audio-policy.json',self.profiles,self.tts_tests,self.stt,self.scheduler,self.endpoint) self.sessions = {} self.login_attempts = [] self.auth_lock = threading.Lock() def video_browser_auth(headers): from http.cookies import SimpleCookie try: cookies=SimpleCookie();cookies.load(headers.get('Cookie',''));token=cookies['deck_session'].value except (KeyError,ValueError):return False record=self.credentials.read() with self.auth_lock: session=self.sessions.get(token) return bool(record and session and session['expires']>time.monotonic() and secrets.compare_digest(session['revision'],record['password']['hash'])) self.endpoint.video_browser_auth=video_browser_auth from backup import Backup self.backup=Backup(self) class Handler(BaseHTTPRequestHandler): def log_message(self, *args): pass def respond(self, data, status=200, mime='application/json'): body = json.dumps(data).encode() if mime == 'application/json' else data self.send_response(status) self.send_header('Content-Type', mime) self.send_header('Content-Length', str(len(body))) self.send_header('Cache-Control', 'no-store') self.send_header('X-Content-Type-Options', 'nosniff') self.send_header('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; frame-ancestors 'none'") self.end_headers() self.wfile.write(body) def ingress(self): token = os.environ.get('DECK_PROXY_TOKEN', '') if token and secrets.compare_digest(self.headers.get('X-Deck-Proxy', ''), token): return self.headers.get('X-Deck-Ingress', 'management') return 'management' def api_token_authenticated(self): header = self.headers.get('Authorization', '') if not header.startswith('Bearer ') or len(header)>300: return False record = self.server.credentials.read() if not record or not record['api_token_hash']: return False actual = hashlib.sha256(header[7:].encode()).hexdigest() return secrets.compare_digest(actual,record['api_token_hash']) def token_route(self): return self.command == 'GET' and self.path in ('/api/v1/status','/api/v1/hardware') def session_token(self): try: return SimpleCookie(self.headers.get('Cookie',''))['deck_session'].value except (KeyError, ValueError, CookieError): return None def authenticated(self, session_only=False): if self.headers.get('Authorization'): return not session_only and self.token_route() and self.api_token_authenticated() record = self.server.credentials.read() if not record: return False with self.server.auth_lock: session = self.server.sessions.get(self.session_token()) return bool(session and session['expires']>time.monotonic() and secrets.compare_digest(session['revision'],record['password']['hash'])) def allow_password_attempt(self): with self.server.auth_lock: now = time.monotonic() self.server.login_attempts = [t for t in self.server.login_attempts if now-t<60] if len(self.server.login_attempts)>=10: self.respond({'error':'Zu viele Versuche. Bitte eine Minute warten.'},429) return False self.server.login_attempts.append(now) return True def read_json(self): self.connection.settimeout(10) if self.headers.get('Transfer-Encoding'): raise ValueError('Chunked Uploads werden nicht unterstützt.') length = int(self.headers.get('Content-Length', '0')) if not 0 < length <= 32768 or self.headers.get('Content-Type') != 'application/json': raise ValueError('JSON-Anfrage erwartet, maximal 32 KiB.') raw = self.rfile.read(length) try: value = json.loads(raw) except (ValueError, UnicodeDecodeError): raise ValueError('Ungültige JSON-Anfrage.') from None if not isinstance(value, dict): raise ValueError('JSON-Objekt erwartet.') return value def login(self): if not self.allow_password_attempt(): return try: data = self.read_json() with self.server.credentials.lock: record = self.server.credentials.read() if not record or not verify_password(data.get('password'),record['password']): return self.respond({'error':'Anmeldung fehlgeschlagen.'},401) token = secrets.token_urlsafe(32) now = time.monotonic() with self.server.auth_lock: self.server.sessions = {k:v for k,v in self.server.sessions.items() if v['expires']>now} if len(self.server.sessions)>=64: self.server.sessions.pop(next(iter(self.server.sessions))) self.server.sessions[token] = dict(expires=now+28800,revision=record['password']['hash']) body = b'{"authenticated":true}' self.send_response(200) self.send_header('Content-Type','application/json') self.send_header('Cache-Control','no-store') self.send_header('Content-Length',str(len(body))) self.send_header('Set-Cookie',f'deck_session={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800') self.end_headers() self.wfile.write(body) except (ValueError, OSError): self.respond({'error':'Ungültige Anmeldung.'},400) def access_status(self): record = self.server.credentials.read() signed_in = self.authenticated(session_only=True) value = dict(initialized=record is not None, authenticated=signed_in) if signed_in: value.update(api_token_configured=bool(record['api_token_hash']), password_changed_at=record['password_changed_at'], token_changed_at=record['token_changed_at'], account='Administrator', location=os.environ.get('DECK_LOCATION', 'Athena · Debian-Server')) return self.respond(value) def change_credentials(self, kind): if not self.allow_password_attempt(): return try: data = self.read_json() key = 'new_password' if kind == 'password' else 'new_token' if set(data) != {'current_password',key}: raise ValueError('Ungültige Zugangsdatenanfrage.') self.server.credentials.change(kind,data['current_password'],data[key]) if kind == 'password': with self.server.auth_lock: self.server.sessions.clear() return self.respond({'changed':True,'login_required':kind=='password'}) except ValueError as exc: return self.respond({'error':str(exc)},400) except OSError: return self.respond({'error':'Zugangsdaten konnten nicht gespeichert werden.'},503) def valid_host(self): if os.environ.get('DECK_PROXY_TOKEN'): return self.ingress() in ('lan','tunnel') allowed = {f'127.0.0.1:{self.server.server_port}', f'localhost:{self.server.server_port}'} allowed.update(filter(None,os.environ.get('DECK_ALLOWED_HOSTS','').split(','))) return self.headers.get('Host') in allowed def execution_setup(self, model, profile=None): installed={'stt':bool(self.server.runtime.status().get('active')),'chat':bool(self.server.runtime.status().get('active')),'image':self.server.image_runtime.status().get('installed',False),'audio':self.server.tts_runtime.status().get('installed',False),'video':self.server.video.status()['runtime']['installed'],'audio_cpp':self.server.audio_cpp_runtime.status()['installed'],'music_worker':True} return execution_assess(model,installed,profile) def do_GET(self): if not self.valid_host(): return self.respond({'error': 'Host rejected'}, 403) if self.path == '/api/v1/auth/status': return self.access_status() if self.path == '/login.js': return self.respond((ROOT/'login.js').read_bytes(), mime='text/javascript') if not self.authenticated() and self.path not in ('/', '/style.css'): return self.respond({'error':'Anmeldung erforderlich.'},401) if not self.authenticated() and self.path == '/': return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8') routes = {'/music-ui.js':('music-ui.js','text/javascript'),'/audio-cpp-ui.js':('audio-cpp-ui.js','text/javascript'),'/ltx-original-ui.js':('ltx-original-ui.js','text/javascript'),'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')} if self.path in routes: name, mime = routes[self.path] return self.respond((ROOT/name).read_bytes(), mime=mime) if self.path == '/api/v1/status': endpoint=self.server.endpoint.status() public_endpoint={key:endpoint[key] for key in ('state','port','counts','active_requests')} return self.respond(dict(name='Athena Deck', version='0.7.0', state='ready', uptime_seconds=round(time.time()-self.server.started), mode='isolated', location=os.environ.get('DECK_LOCATION', 'Athena · Debian-Server'), endpoint=public_endpoint)) if self.path == '/api/v1/auto-tests':return self.respond(self.server.auto_tests.status()) if self.path == '/api/v1/backup':return self.respond(self.server.backup.status()) if self.path == '/api/v1/chat-tests':return self.respond(self.server.chat_tests.status()) if self.path == '/api/v1/endpoint':return self.respond(self.server.endpoint.status()) if self.path == '/api/v1/docker':return self.respond(self.server.docker.status()) if self.path == '/api/v1/stt':return self.respond(self.server.stt.status()) if self.path == '/api/v1/tts':return self.respond(self.server.tts_tests.status()) if self.path == '/api/v1/audio-policy':return self.respond(self.server.audio_policy.status()) if urlsplit(self.path).path == '/api/v1/tts/audio': try:return self.respond(self.server.tts_tests.audio(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='audio/wav') except (OSError,ValueError):return self.respond({'error':'Audio nicht verfügbar.'},404) if self.path == '/api/v1/music':return self.respond(self.server.music.status()) if urlsplit(self.path).path == '/api/v1/music/audio': try:return self.respond(self.server.music.audio(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='audio/wav') except (OSError,ValueError):return self.respond({'error':'Musik nicht verfügbar.'},404) if self.path == '/api/v1/audio-cpp-runtime':return self.respond(self.server.audio_cpp_runtime.status()) if self.path == '/api/v1/audio-cpp-runtime/log':return self.respond(self.server.audio_cpp_runtime.log()) if self.path == '/api/v1/ltx-original-runtime':return self.respond(self.server.ltx_original_runtime.status()) if self.path == '/api/v1/ltx-original-runtime/log':return self.respond(self.server.ltx_original_runtime.log()) if self.path == '/api/v1/video':return self.respond(self.server.video.status()) if self.path == '/api/v1/image-runtime':return self.respond(self.server.image_runtime.status()) if self.path == '/api/v1/image-tests':return self.respond(self.server.image_tests.status()) if self.path == '/api/v1/prompt-enhancers':return self.respond(self.server.prompt_enhancer.status()) if urlsplit(self.path).path == '/api/v1/image-tests/image': try:return self.respond(self.server.image_tests.image(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='image/png') except (OSError,ValueError):return self.respond({'error':'Bild nicht verfügbar.'},404) if urlsplit(self.path).path == '/api/v1/profiles/components': try:return self.respond(self.server.profiles.components(parse_qs(urlsplit(self.path).query).get('model_id',[''])[0],parse_qs(urlsplit(self.path).query).get('repo',[None])[0])) except ValueError as exc:return self.respond({'error':str(exc)},400) except Exception:return self.respond({'error':'Komponentenquelle nicht erreichbar. Bitte erneut versuchen.'},503) if self.path == '/api/v1/profiles': data=self.server.profiles.status() for p in data['profiles']:p['execution']=self.execution_setup(p.get('model') or {},p) return self.respond(data) if self.path == '/api/v1/huggingface':return self.respond(self.server.catalog.hub_auth.status()) if self.path == '/api/v1/hardware': return self.respond(self.server.hardware.snapshot()) if self.path == '/api/v1/dashboard':return self.respond(self.server.dashboard.snapshot()) if urlsplit(self.path).path == '/api/v1/dashboard/history': period=parse_qs(urlsplit(self.path).query).get('range',['24h'])[0] return self.respond(self.server.dashboard.history.query(period)) if self.path == '/api/v1/dashboard/backups':return self.respond(self.server.dashboard.backups()) if self.path.startswith('/api/v1/dashboard/backups/download/'): try:path=self.server.dashboard.backup_file(unquote(self.path.rsplit('/',1)[-1])) except ValueError:return self.respond({'error':'Backup nicht verfügbar.'},404) self.send_response(200) self.send_header('Content-Type','application/octet-stream') self.send_header('Content-Length',str(path.stat().st_size)) self.send_header('Content-Disposition','attachment; filename="'+path.name+'"') self.send_header('Cache-Control','no-store') self.send_header('X-Content-Type-Options','nosniff') self.end_headers() with path.open('rb') as stream: while chunk:=stream.read(1024*1024):self.wfile.write(chunk) return if self.path.startswith('/api/v1/runtime'): actions={'/api/v1/runtime':self.server.runtime.status,'/api/v1/runtime/prerequisites':self.server.runtime.prerequisites,'/api/v1/runtime/releases':self.server.runtime.releases,'/api/v1/runtime/log':self.server.runtime.log,'/api/v1/runtime/references':self.server.runtime.references} if self.path in actions: try:return self.respond(actions[self.path]()) except Exception:return self.respond({'error':'Laufzeitabfrage fehlgeschlagen; Verbindung oder Werkzeuge prüfen.'},503) if self.path.startswith('/api/v1/catalog'): try: path=urlsplit(self.path);q=parse_qs(path.query) if path.path=='/api/v1/catalog/search': base=q.get('base_only',['false'])[0] if base not in ('true','false'):raise ValueError('Ungültiger Basismodellfilter.') return self.respond(self.server.catalog.search(q.get('q',[''])[0],q.get('kind',['chat'])[0],q.get('sort',['downloads'])[0],base=='true',q.get('purpose',['model'])[0])) if path.path=='/api/v1/catalog/assessment': data=self.server.catalog.files(q.get('repo',[''])[0]) return self.respond(overview(data['files'],self.server.hardware.snapshot())) if path.path=='/api/v1/catalog/files': data=dict(self.server.catalog.files(q.get('repo',[''])[0]));hardware=self.server.hardware.snapshot() data['files']=[dict(f,assessment=assess(f['size'],hardware,f['name']),execution=self.execution_setup(dict(repo=data['repo'],revision=data['revision'],file=f['name'],kind=q.get('kind',['chat'])[0],runtime_metadata=data.get('runtime_metadata',{})))) for f in data['files']] from profiles import component_recipe from catalog import file_role for f in data['files']: model=dict(repo=data['repo'],file=f['name'],kind=q.get('kind',['chat'])[0]) recipe=component_recipe(model) or self.server.profiles.runtime_recipe(model) if recipe:f['execution']['components']=[r['label'] for r in recipe.values()] elif f['execution']['state']!='component':f['execution']['suggested_components']=[x['name'] for x in data['files'] if file_role(x['name'])['role'] in ('text_encoder','vae','vision_projector','audio_projector')][:12] return self.respond(data) if path.path=='/api/v1/catalog': data=self.server.catalog.status() for entry in data['entries']: entry['execution']=self.execution_setup(entry) entry['used_by_profiles']=self.server.profiles.references(entry['id']) return self.respond(data) except Exception as exc: return self.respond({'error':str(exc) if isinstance(exc,ValueError) else 'Katalog nicht erreichbar.'},400) if self.path == '/api/v1/network': return self.respond(self.server.network.status(self.ingress())) return self.respond({'error': 'Not found'}, 404) def do_POST(self): origin = self.headers.get('Origin') bearer = self.token_route() and self.api_token_authenticated() if not self.valid_host() or (origin and origin != 'http://' + self.headers.get('Host', '')) or (not bearer and self.headers.get('X-Athena-Deck') != '1'): return self.respond({'error':'Same-origin control required'},403) if self.path == '/api/v1/auth/setup': try: data = self.read_json() if set(data) != {'password','api_token'}: raise ValueError('Kennwort und API-Token werden benötigt.') # Container installs always arrive provisioned. Never offer remote claim-on-first-use. if os.environ.get('DECK_AUTH_RPC') or os.environ.get('DECK_REQUIRE_SETUP') == '1': return self.respond({'error':'Serverzugang wird bei der Installation eingerichtet.'},403) self.server.credentials.setup(data['password'],data['api_token']) return self.respond({'initialized':True}) except ValueError as exc: return self.respond({'error':str(exc)},400) except OSError: return self.respond({'error':'Einrichtung konnte nicht gespeichert werden.'},503) if self.path == '/api/v1/login': return self.login() if not self.authenticated(): return self.respond({'error':'Anmeldung oder gültiger API-Token erforderlich.'},401) if self.server.backup.busy() and not self.path.startswith('/api/v1/backup/') and self.path != '/api/v1/logout': return self.respond({'error':'Wiederherstellung läuft. Änderungen und Tests sind vorübergehend gesperrt.'},409) if self.path.startswith('/api/v1/backup/'): try: if not self.authenticated(session_only=True):return self.respond({'error':'Administratorsitzung erforderlich.'},401) action=self.path.rsplit('/',1)[1] if action=='export': data=self.read_json() if set(data)!={'password','theme','history'} or type(data['history']) is not bool:raise ValueError('Backup-Kennwort, Theme und History-Auswahl erforderlich.') body=self.server.backup.export(data['password'],data['theme'],data['history']) self.send_response(200);self.send_header('Content-Type','application/octet-stream');self.send_header('Content-Length',str(len(body)));self.send_header('Content-Disposition','attachment; filename="athena-deck-'+time.strftime('%Y%m%d-%H%M%S')+'.adbackup"');self.send_header('Cache-Control','no-store');self.end_headers();self.wfile.write(body);return if action=='inspect': from backup_codec import MAX self.connection.settimeout(60) length=int(self.headers.get('Content-Length','0')) if self.headers.get('Transfer-Encoding') or self.headers.get('Content-Type')!='application/octet-stream' or not 0