diff --git a/api.py b/api.py new file mode 100644 index 0000000..3a20322 --- /dev/null +++ b/api.py @@ -0,0 +1,56 @@ +import requests +from util import * + +class ApiError(Exception): + def __init__(self, msg, code): + super().__init__(msg) + self.code = code + +class Api: + def __init__(self, store, agent): + self.agent = agent + self.store = store + self.root = 'https://api.spacetraders.io/v2/' + + def token(self): + if self.agent.token is None: + raise ApiError('no token. Please register', 1337) + return self.agent.token + + def request(self, method, path, data=None, need_token=True): + headers = {} + params = {} + if need_token: + headers['Authorization'] = 'Bearer ' + self.token() + if method == 'get': + params['limit'] = 20 + r = requests.request(method, self.root+path, json=data, headers=headers, params=params) + + result = r.json() + self.last_result = result + if result is None: + raise ApiError('http call failed', r.status_code) + elif 'data' not in result: + error_code = sg(result, 'error.code', -1) + self.last_error = error_code + error_message = sg(result, 'error.message') + raise ApiError(error_message, error_code) + else: + self.last_error = 0 + return result['data'] + + def register(self, faction): + callsign = self.agent.symbol + data = { + 'symbol': callsign, + 'faction': faction + } + result = self.request('post', 'register', data, need_token=False) + token = mg(result, 'token') + self.agent.token = token + + def info(self): + data = self.request('get', 'my/agent') + self.agent.update(data) + return self.agent + diff --git a/command_line.py b/command_line.py index 050b5b2..94f67e5 100644 --- a/command_line.py +++ b/command_line.py @@ -2,6 +2,7 @@ import shlex import inspect import sys import importlib +import logging def func_supports_argcount(f, cnt): argspec = inspect.getargspec(f) @@ -34,10 +35,13 @@ class CommandLine: print(f'command not found; {c}') def handle_error(self, cmd, args, e): - print(e) + logging.error(e, exc_info=str(type(e))=='ApiError') def handle_empty(self): pass + + def after_cmd(self): + pass def do_quit(self): print('byebye!') @@ -67,6 +71,7 @@ class CommandLine: handler(*args) except Exception as e: self.handle_error(cmd, args, e) + self.after_cmd() def run(self): while not self.stopping and not self.reloading: diff --git a/commander.py b/commander.py index 2883d54..cef7ef4 100644 --- a/commander.py +++ b/commander.py @@ -1,23 +1,51 @@ from store import Store from command_line import CommandLine import argparse +from models.agent import Agent +from api import Api +from util import * + class Commander(CommandLine): - def __init__(self, store_dir): + def __init__(self, store_dir='data', agent=None): self.store_dir = store_dir self.store = Store(store_dir) + self.agent = self.select_agent(agent) + self.api = Api(self.store, self.agent) + self.store.flush() super().__init__() - + + def select_agent(self, agent_str): + if agent_str is not None: + return self.store.get(Agent, agent_str) + else: + agents = self.store.all('', Agent) + agent = next(agents, None) + if agent is None: + symbol = input('agent name: ') + agent = self.store.get(Agent, symbol) + return agent + + def after_cmd(self): + self.store.flush() + def do_foo(self): self.store.foo() self.store.flush() + + def do_info(self): + pprint(self.api.info(), 100) + + def do_register(self, faction): + self.api.register(faction.upper()) def main(args): - c = Commander(args.store_dir) + c = Commander(args.store_dir, args.agent) c.run() if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-s', '--store-dir', default='data') + parser.add_argument('-a', '--agent', default=None) args = parser.parse_args() main(args) diff --git a/models/agent.py b/models/agent.py new file mode 100644 index 0000000..9a6ca80 --- /dev/null +++ b/models/agent.py @@ -0,0 +1,21 @@ +from .base import Base + +class Agent(Base): + token: str = None + credits: int = 0 + + def update(self, d): + self.seta(d, 'credits') + + def path(self): + return f'{self.symbol}.{self.ext()}' + + @classmethod + def ext(self): + return 'agt' + + def f(self, detail=1): + r = super().f(detail) + if detail >2: + r += f' c:{self.credits}' + return r diff --git a/models/base.py b/models/base.py index b1e39be..294c6b6 100644 --- a/models/base.py +++ b/models/base.py @@ -1,13 +1,28 @@ from copy import deepcopy +from dataclasses import dataclass +@dataclass class Base: symbol: str + dirty: bool def __init__(self, symbol, store): self.symbol = symbol self.store = store self.dirty = True + def seta(self, d, name): + if name in d: + setattr(self, name, d[name]) + + def __setattr__(self, name, value): + if name != 'dirty': + self.dirty = True + super().__setattr__(name, value) + + def update(self, d): + pass + def dict(self): r = deepcopy(self.__dict__) del r['store'] @@ -16,7 +31,8 @@ class Base: def path(self): raise NotImplementedError('path') - + + @classmethod def ext(self): raise NotImplementedError('extension') @@ -24,4 +40,10 @@ class Base: return self.__class__.__name__ def __str__(self): - return f'{self.symbol}.{self.ext()}' + return self.f() + + def f(self, detail=1): + r = self.symbol + if detail > 1: + r += '.' + self.ext() + return r diff --git a/models/sector.py b/models/sector.py index 4b40969..574a863 100644 --- a/models/sector.py +++ b/models/sector.py @@ -1,5 +1,6 @@ from .base import Base class Sector(Base): + @classmethod def ext(self): return 'sct' diff --git a/models/setting.py b/models/setting.py deleted file mode 100644 index cf57116..0000000 --- a/models/setting.py +++ /dev/null @@ -1,8 +0,0 @@ -from .base import Base - - -class Setting(Base): - name: str - value: str - def ext(self): - return 'set' diff --git a/models/system.py b/models/system.py index 5778003..371ec75 100644 --- a/models/system.py +++ b/models/system.py @@ -3,6 +3,7 @@ from .base import Base class System(Base): + @classmethod def ext(self): return 'stm' diff --git a/models/waypoint.py b/models/waypoint.py index 13dc849..b84e5a4 100644 --- a/models/waypoint.py +++ b/models/waypoint.py @@ -2,5 +2,6 @@ from .base import Base class Waypoint(Base): + @classmethod def ext(self): return 'way' diff --git a/store.py b/store.py index 16c481a..9b84ada 100644 --- a/store.py +++ b/store.py @@ -2,7 +2,7 @@ from models.base import Base from models.waypoint import Waypoint from models.sector import Sector from models.system import System -from models.setting import Setting +from models.agent import Agent from os.path import isfile, dirname, isdir import os import json @@ -22,6 +22,7 @@ class Store: with open(path) as f: data = json.load(f) data['store'] = self + data['dirty'] = False obj.__dict__ = data def store(self, obj): @@ -40,9 +41,27 @@ class Store: self.data[symbol] = obj return obj + def all(self, path, typ): + if hasattr(path, 'path'): + path = path.path() + path = os.path.join(self.data_dir, path) + if not isdir(path): + return + ext = '.' + typ.ext() + for f in os.listdir(path): + fil = os.path.join(path, f) + if not isfile(fil): + continue + if not fil.endswith(ext): + continue + symbol = f[:-len(ext)] + yield self.get(typ, symbol) + + def flush(self): for obj in self.data.values(): - self.store(obj) + if obj.dirty: + self.store(obj) def foo(self): s = self.get(System, 'dez-hq14') diff --git a/util.py b/util.py new file mode 100644 index 0000000..10a7d0a --- /dev/null +++ b/util.py @@ -0,0 +1,67 @@ +from datetime import datetime + +def must_get(d, k): + if type(k) == str: + k = k.split('.') + part = k.pop(0) + if type(d) != dict or part not in d: + raise ValueError(part + ' not found') + val = d[part] + if len(k) == 0: + return val + else: + return must_get(val, k) + +mg = must_get + +def should_get(d, k, default=None): + try: + return must_get(d, k) + except ValueError: + return default + +sg = should_get + +def find_where_eq(l, k, v): + for i in l: + if k in i and i[k] == v: + return i + return None + +def all_subclasses(cls): + return set(cls.__subclasses__()).union( + [s for c in cls.__subclasses__() for s in all_subclasses(c)]) + +def pprint(d, detail=2): + print(pretty(d, detail=detail)) + +def pretty(d, ident=0, detail=2): + if type(d) in [int, str, float, bool]: + return str(d) + if hasattr(d, 'f'): + return d.f(detail) + r = '' + idt = ' ' * ident + if type(d) == list: + r += 'lst' + for i in d: + r += '\n' + idt + pretty(i, ident + 1, detail) + elif type(d) == dict: + r += 'map' + for k,v in d.items(): + r += '\n' + idt + k + ': ' + pretty(v, ident + 1, detail) + return r + +def trim(s, l): + s = s[:l] + s += ' ' * (l-len(s)) + return s + +# >>> parse_timestamp('2023-06-02T20:34:48.293Z') +# 1685738088 +def parse_timestamp(ts): + return int(datetime.strptime(ts, '%Y-%m-%dT%H:%M:%S.%f%z').timestamp()) + +def render_timestamp(ts): + return datetime.utcfromtimestamp(ts).isoformat() +