|
1
|
|
|
#!/usr/bin/env python |
|
2
|
|
|
|
|
3
|
|
|
"""REST server to display content and reserve item numbers.""" |
|
4
|
|
|
|
|
5
|
|
|
import os |
|
6
|
|
|
from collections import defaultdict |
|
7
|
|
|
import webbrowser |
|
8
|
|
|
import argparse |
|
9
|
|
|
import logging |
|
10
|
|
|
|
|
11
|
|
|
import bottle |
|
12
|
|
|
from bottle import get, post, request |
|
13
|
|
|
|
|
14
|
|
|
from doorstop import common, build, publisher |
|
15
|
|
|
from doorstop.common import HelpFormatter |
|
16
|
|
|
from doorstop.server import utilities |
|
17
|
|
|
from doorstop import settings |
|
18
|
|
|
|
|
19
|
|
|
log = common.logger(__name__) |
|
20
|
|
|
|
|
21
|
|
|
app = utilities.StripPathMiddleware(bottle.app()) |
|
22
|
|
|
tree = None # set in `run`, read in the route functions |
|
23
|
|
|
numbers = defaultdict(int) # cache of next document numbers |
|
24
|
|
|
|
|
25
|
|
|
|
|
26
|
|
|
def main(args=None): |
|
27
|
|
|
"""Process command-line arguments and run the program.""" |
|
28
|
|
|
from doorstop import SERVER, VERSION |
|
|
|
|
|
|
29
|
|
|
|
|
30
|
|
|
# Shared options |
|
31
|
|
|
debug = argparse.ArgumentParser(add_help=False) |
|
32
|
|
|
debug.add_argument('-V', '--version', action='version', version=VERSION) |
|
33
|
|
|
debug.add_argument('--debug', action='store_true', help=argparse.SUPPRESS) |
|
34
|
|
|
debug.add_argument('--launch', action='store_true', help=argparse.SUPPRESS) |
|
35
|
|
|
shared = {'formatter_class': HelpFormatter, 'parents': [debug]} |
|
36
|
|
|
|
|
37
|
|
|
# Build main parser |
|
38
|
|
|
parser = argparse.ArgumentParser(prog=SERVER, description=__doc__, |
|
39
|
|
|
**shared) |
|
40
|
|
|
parser.add_argument('-j', '--project', metavar="PATH", |
|
41
|
|
|
help="path to the root of the project") |
|
42
|
|
|
parser.add_argument('-P', '--port', metavar='NUM', type=int, |
|
43
|
|
|
help="use a custom port for the server") |
|
44
|
|
|
|
|
45
|
|
|
# Parse arguments |
|
46
|
|
|
args = parser.parse_args(args=args) |
|
47
|
|
|
|
|
48
|
|
|
# Configure logging |
|
49
|
|
|
logging.basicConfig(format=settings.VERBOSE_LOGGING_FORMAT, |
|
50
|
|
|
level=settings.VERBOSE_LOGGING_LEVEL) |
|
51
|
|
|
|
|
52
|
|
|
# Run the program |
|
53
|
|
|
run(args, os.getcwd(), parser.error) |
|
54
|
|
|
|
|
55
|
|
|
|
|
56
|
|
|
def run(args, cwd, _): |
|
57
|
|
|
"""Start the server. |
|
58
|
|
|
|
|
59
|
|
|
:param args: Namespace of CLI arguments (from this module or the CLI) |
|
60
|
|
|
:param cwd: current working directory |
|
61
|
|
|
:param error: function to call for CLI errors |
|
62
|
|
|
|
|
63
|
|
|
""" |
|
64
|
|
|
global tree # pylint: disable=W0603,C0103 |
|
65
|
|
|
tree = build(cwd=cwd, root=args.project) |
|
66
|
|
|
tree.load() |
|
67
|
|
|
host = 'localhost' |
|
68
|
|
|
port = args.port or settings.SERVER_PORT |
|
69
|
|
|
if args.launch: |
|
70
|
|
|
url = utilities.build_url(host=host, port=port) |
|
71
|
|
|
webbrowser.open(url) |
|
72
|
|
|
bottle.run(app=app, host=host, port=port, |
|
73
|
|
|
debug=args.debug, reloader=args.debug) |
|
74
|
|
|
|
|
75
|
|
|
|
|
76
|
|
|
@get('/') |
|
77
|
|
|
def index(): |
|
78
|
|
|
"""Read the tree.""" |
|
79
|
|
|
yield '<pre><code>' |
|
80
|
|
|
yield tree.draw() |
|
81
|
|
|
yield '</pre></code>' |
|
82
|
|
|
|
|
83
|
|
|
|
|
84
|
|
|
@get('/documents') |
|
85
|
|
|
def get_documents(): |
|
86
|
|
|
"""Read the tree's documents.""" |
|
87
|
|
|
prefixes = [str(document.prefix) for document in tree] |
|
88
|
|
|
if utilities.json_response(request): |
|
89
|
|
|
data = {'prefixes': prefixes} |
|
90
|
|
|
return data |
|
91
|
|
|
else: |
|
92
|
|
|
return '<br>'.join(prefixes) |
|
93
|
|
|
|
|
94
|
|
|
|
|
95
|
|
|
@get('/documents/<prefix>') |
|
96
|
|
|
def get_document(prefix): |
|
97
|
|
|
"""Read a tree's document.""" |
|
98
|
|
|
document = tree.find_document(prefix) |
|
99
|
|
|
if utilities.json_response(request): |
|
100
|
|
|
data = {str(item.uid): item.data for item in document} |
|
101
|
|
|
return data |
|
102
|
|
|
else: |
|
103
|
|
|
return publisher.publish_lines(document, ext='.html') |
|
104
|
|
|
|
|
105
|
|
|
|
|
106
|
|
|
@get('/documents/<prefix>/items') |
|
107
|
|
|
def get_items(prefix): |
|
108
|
|
|
"""Read a document's items.""" |
|
109
|
|
|
document = tree.find_document(prefix) |
|
110
|
|
|
uids = [str(item.uid) for item in document] |
|
111
|
|
|
if utilities.json_response(request): |
|
112
|
|
|
data = {'uids': uids} |
|
113
|
|
|
return data |
|
114
|
|
|
else: |
|
115
|
|
|
return '<br>'.join(uids) |
|
116
|
|
|
|
|
117
|
|
|
|
|
118
|
|
|
@get('/documents/<prefix>/items/<uid>') |
|
119
|
|
|
def get_item(prefix, uid): |
|
120
|
|
|
"""Read a document's item.""" |
|
121
|
|
|
document = tree.find_document(prefix) |
|
122
|
|
|
item = document.find_item(uid) |
|
123
|
|
|
if utilities.json_response(request): |
|
124
|
|
|
return {'data': item.data} |
|
125
|
|
|
else: |
|
126
|
|
|
return publisher.publish_lines(item, ext='.html') |
|
127
|
|
|
|
|
128
|
|
|
|
|
129
|
|
|
@get('/documents/<prefix>/items/<uid>/attrs') |
|
130
|
|
|
def get_attrs(prefix, uid): |
|
131
|
|
|
"""Read an item's attributes.""" |
|
132
|
|
|
document = tree.find_document(prefix) |
|
133
|
|
|
item = document.find_item(uid) |
|
134
|
|
|
attrs = sorted(item.data.keys()) |
|
135
|
|
|
if utilities.json_response(request): |
|
136
|
|
|
data = {'attrs': attrs} |
|
137
|
|
|
return data |
|
138
|
|
|
else: |
|
139
|
|
|
return '<br>'.join(attrs) |
|
140
|
|
|
|
|
141
|
|
|
|
|
142
|
|
|
@get('/documents/<prefix>/items/<uid>/attrs/<name>') |
|
143
|
|
|
def get_attr(prefix, uid, name): |
|
144
|
|
|
"""Read an item's attribute value.""" |
|
145
|
|
|
document = tree.find_document(prefix) |
|
146
|
|
|
item = document.find_item(uid) |
|
147
|
|
|
value = item.data.get(name, None) |
|
148
|
|
|
if utilities.json_response(request): |
|
149
|
|
|
data = {'value': value} |
|
150
|
|
|
return data |
|
151
|
|
|
else: |
|
152
|
|
|
if isinstance(value, str): |
|
153
|
|
|
return value |
|
154
|
|
|
try: |
|
155
|
|
|
return '<br>'.join(str(e) for e in value) |
|
156
|
|
|
except TypeError: |
|
157
|
|
|
return str(value) |
|
158
|
|
|
|
|
159
|
|
|
|
|
160
|
|
|
@post('/documents/<prefix>/numbers') |
|
161
|
|
|
def post_numbers(prefix): |
|
162
|
|
|
"""Create the next number in a document.""" |
|
163
|
|
|
document = tree.find_document(prefix) |
|
164
|
|
|
number = max(document.next_number, numbers[prefix]) |
|
|
|
|
|
|
165
|
|
|
numbers[prefix] = number + 1 |
|
166
|
|
|
if utilities.json_response(request): |
|
167
|
|
|
data = {'next': number} |
|
168
|
|
|
return data |
|
169
|
|
|
else: |
|
170
|
|
|
return str(number) |
|
171
|
|
|
|
|
172
|
|
|
|
|
173
|
|
|
if __name__ == '__main__': # pragma: no cover (manual test) |
|
174
|
|
|
main() |
|
175
|
|
|
|