Skip to content

Commit f7b5103

Browse files
Merge pull request #41 from ActivityWatch/dev/import
Implemented import API endpoint
2 parents 1ab2ed4 + daa9e0b commit f7b5103

2 files changed

Lines changed: 80 additions & 27 deletions

File tree

aw_server/api.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Any, Optional
1+
from typing import Dict, List, Any, Optional, Union
22
from datetime import datetime
33
from socket import gethostname
44
import functools
@@ -33,7 +33,7 @@ class ServerAPI:
3333
def __init__(self, db, testing) -> None:
3434
self.db = db
3535
self.testing = testing
36-
self.last_event = {} #type: dict
36+
self.last_event = {} # type: dict
3737

3838
def get_info(self) -> Dict[str, Dict]:
3939
"""Get server info"""
@@ -73,19 +73,49 @@ def export_bucket(self, bucket_id: str) -> Dict[str, Any]:
7373
del event["id"]
7474
return bucket
7575

76-
def create_bucket(self, bucket_id: str, event_type: str, client: str, hostname: str) -> bool:
76+
def export_all(self) -> Dict[str, Any]:
77+
"""Exports all buckets and their events to a format consistent across versions"""
78+
buckets = self.get_buckets()
79+
exported_buckets = {}
80+
for bid in buckets.keys():
81+
exported_buckets[bid] = self.export_bucket(bid)
82+
return exported_buckets
83+
84+
def import_bucket(self, bucket_data: Any):
85+
bucket_id = bucket_data["id"]
86+
logger.info("Importing bucket {}".format(bucket_id))
87+
# TODO: Check that bucket doesn't already exist
88+
self.db.create_bucket(
89+
bucket_id,
90+
type=bucket_data["type"],
91+
client=bucket_data["client"],
92+
hostname=bucket_data["hostname"],
93+
created=(bucket_data["created"]
94+
if isinstance(bucket_data["created"], datetime)
95+
else iso8601.parse_date(bucket_data["created"])),
96+
)
97+
self.create_events(bucket_id, [Event(**e) if isinstance(e, dict) else e for e in bucket_data["events"]])
98+
99+
def import_all(self, buckets: Dict[str, Any]):
100+
for bid, bucket in buckets.items():
101+
self.import_bucket(bucket)
102+
103+
def create_bucket(self, bucket_id: str, event_type: str, client: str,
104+
hostname: str, created: Optional[datetime] = None)-> bool:
77105
"""
78106
Create bucket.
79107
Returns True if successful, otherwise false if a bucket with the given ID already existed.
80108
"""
109+
if created is None:
110+
created = datetime.now()
81111
if bucket_id in self.db.buckets():
82112
return False
83113
self.db.create_bucket(
84114
bucket_id,
85115
type=event_type,
86116
client=client,
87117
hostname=hostname,
88-
created=datetime.now()
118+
created=created
89119
)
90120
return True
91121

@@ -207,10 +237,3 @@ def get_log(self):
207237
for line in log_file.readlines()[::-1]:
208238
payload.append(json.loads(line))
209239
return payload, 200
210-
211-
def export_all(self) -> Dict[str, dict]:
212-
"""Exports all buckets and their events to a format consistent across versions"""
213-
buckets = self.get_buckets()
214-
for bid in buckets.keys():
215-
buckets[bid] = self.export_bucket(bid)
216-
return buckets

aw_server/rest.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import traceback
33
import json
44

5-
from flask import request, Blueprint, jsonify
5+
from flask import request, Blueprint, jsonify, make_response
66
from flask_restplus import Api, Resource, fields
77
import iso8601
88
from datetime import datetime, timedelta
@@ -26,6 +26,7 @@
2626
blueprint = Blueprint('api', __name__, url_prefix='/api')
2727
api = Api(blueprint, doc='/')
2828

29+
2930
# TODO: Clean up JSONEncoder code?
3031
class CustomJSONEncoder(json.JSONEncoder):
3132
def __init__(self, *args, **kwargs):
@@ -40,6 +41,8 @@ def default(self, obj, *args, **kwargs):
4041
except TypeError:
4142
pass
4243
return json.JSONEncoder.default(self, obj)
44+
45+
4346
app.json_encoder = CustomJSONEncoder
4447

4548
app.register_blueprint(blueprint)
@@ -54,37 +57,32 @@ def format(self, value):
5457
return json.loads(value)
5558

5659

57-
# Loads event schema from JSONSchema in aw_core
60+
# Loads event and bucket schema from JSONSchema in aw_core
5861
event = api.schema_model('Event', schema.get_json_schema("event"))
62+
bucket = api.schema_model('Bucket', schema.get_json_schema("bucket"))
63+
buckets_export = api.schema_model('Export', schema.get_json_schema("export"))
5964

6065
# TODO: Construct all the models from JSONSchema?
6166
# A downside to contructing from JSONSchema: flask-restplus does not have marshalling support
67+
6268
info = api.model('Info', {
6369
'hostname': fields.String(),
6470
'version': fields.String(),
6571
'testing': fields.Boolean(),
6672
})
6773

68-
bucket = api.model('Bucket', {
69-
'id': fields.String(required=True, description='The buckets unique id'),
70-
'name': fields.String(required=False, description='The buckets readable and renameable name'),
71-
'type': fields.String(required=True, description='The buckets event type'),
72-
'client': fields.String(required=True, description='The name of the watcher client'),
73-
'hostname': fields.String(required=True, description='The hostname of the client that the bucket belongs to'),
74-
'created': fields.DateTime(required=True, description='The creation datetime of the bucket'),
75-
})
76-
7774
create_bucket = api.model('CreateBucket', {
7875
'client': fields.String(required=True),
7976
'type': fields.String(required=True),
8077
'hostname': fields.String(required=True),
8178
})
8279

8380
query = api.model('Query', {
84-
'timeperiods': fields.List(fields.String, required=True, description='List of periods to query'),
81+
'timeperiods': fields.List(fields.String, required=True, description='List of periods to query'),
8582
'query': fields.List(fields.String, required=True, description='String list of query statements'),
8683
})
8784

85+
8886
def copy_doc(api_method):
8987
"""Decorator that copies another functions docstring to the decorated function.
9088
Used to copy the docstrings in ServerAPI over to the flask-restplus Resources.
@@ -117,7 +115,7 @@ def get(self) -> Dict[str, Dict]:
117115

118116
@api.route("/0/buckets/<string:bucket_id>")
119117
class BucketResource(Resource):
120-
@api.marshal_with(bucket)
118+
@api.doc(model=bucket)
121119
@copy_doc(ServerAPI.get_bucket_metadata)
122120
def get(self, bucket_id):
123121
return app.api.get_bucket_metadata(bucket_id)
@@ -254,21 +252,53 @@ def post(self):
254252
return {"type": type(qe).__name__, "message": str(qe)}, 400
255253

256254

257-
# EXPORTING
255+
# EXPORT AND IMPORT
256+
258257

259258
@api.route("/0/export")
260259
class ExportAllResource(Resource):
260+
@api.doc(model=buckets_export)
261261
@copy_doc(ServerAPI.export_all)
262262
def get(self):
263-
return app.api.export_all(), 200
263+
buckets_export = app.api.export_all()
264+
payload = {"buckets": buckets_export}
265+
response = make_response(json.dumps(payload))
266+
filename = "aw-buckets-export.json"
267+
response.headers["Content-Disposition"] = "attachment; filename={}".format(filename)
268+
return response
264269

265270

266271
# TODO: Perhaps we don't need this, could be done with a query argument to /0/export instead
267272
@api.route("/0/buckets/<string:bucket_id>/export")
268273
class BucketExportResource(Resource):
274+
@api.doc(model=buckets_export)
269275
@copy_doc(ServerAPI.export_bucket)
270276
def get(self, bucket_id):
271-
return app.api.export_bucket(bucket_id)
277+
bucket_export = app.api.export_bucket(bucket_id)
278+
payload = {"buckets": {bucket_export["id"]: bucket_export}}
279+
response = make_response(json.dumps(payload))
280+
filename = "aw-bucket-export_{}.json".format(bucket_export["id"])
281+
response.headers["Content-Disposition"] = "attachment; filename={}".format(filename)
282+
return response
283+
284+
285+
@api.route("/0/import")
286+
class ImportAllResource(Resource):
287+
@api.expect(buckets_export)
288+
@copy_doc(ServerAPI.import_all)
289+
def post(self):
290+
# If import comes from a form in th web-ui
291+
if len(request.files) > 0:
292+
# web-ui form only allows one file, but technically it's possible to
293+
# upload multiple files at the same time
294+
for filename, f in request.files.items():
295+
buckets = json.loads(f.stream.read())["buckets"]
296+
app.api.import_all(buckets)
297+
# Normal import from body
298+
else:
299+
buckets = request.get_json()["buckets"]
300+
app.api.import_all(buckets)
301+
return None, 200
272302

273303

274304
# LOGGING

0 commit comments

Comments
 (0)