Completed
Push — develop ( e33935...02ef5e )
by Jace
13s queued 11s
created

doorstop/server/main.py (1 issue)

1
#!/usr/bin/env python
2
# SPDX-License-Identifier: LGPL-3.0-only
3 1
4
"""REST server to display content and reserve item numbers."""
5 1
6 1
import argparse
7 1
import logging
8 1
import os
9 1
import webbrowser
10
from collections import defaultdict
11 1
12 1
import bottle
13
from bottle import get, hook, post, request, response, template
14 1
15 1
from doorstop import build, common, publisher, settings
16 1
from doorstop.common import HelpFormatter
17 1
from doorstop.core import vcs
18 1
from doorstop.server import utilities
19
20 1
log = common.logger(__name__)
21
22 1
app = utilities.StripPathMiddleware(bottle.app())
23 1
tree = None  # set in `run`, read in the route functions
24 1
numbers = defaultdict(int)  # cache of next document numbers
25
26
27 1
def main(args=None):
28
    """Process command-line arguments and run the program."""
29 1
    from doorstop import SERVER, VERSION
30
31
    # Shared options
32 1
    debug = argparse.ArgumentParser(add_help=False)
33 1
    debug.add_argument('-V', '--version', action='version', version=VERSION)
34 1
    debug.add_argument('--debug', action='store_true', help=argparse.SUPPRESS)
35 1
    debug.add_argument('--launch', action='store_true', help=argparse.SUPPRESS)
36 1
    shared = {'formatter_class': HelpFormatter, 'parents': [debug]}
37
38
    # Build main parser
39 1
    parser = argparse.ArgumentParser(prog=SERVER, description=__doc__, **shared)
40
    cwd = os.getcwd()
41 1
42 1
    parser.add_argument(
43
        '-j', '--project', default=None, help="path to the root of the project"
44 1
    )
45
    parser.add_argument(
46 1
        '-P',
47
        '--port',
48
        metavar='NUM',
49 1
        type=int,
50
        default=settings.SERVER_PORT,
51
        help="use a custom port for the server",
52
    )
53 1
    parser.add_argument(
54
        '-H', '--host', default='127.0.0.1', help="IP address to listen"
55
    )
56 1
    parser.add_argument(
57
        '-w', '--wsgi', action='store_true', help="Run as a WSGI process"
58
    )
59
    parser.add_argument(
60 1
        '-b',
61
        '--baseurl',
62
        default='',
63 1
        help="Base URL this is served at (Usually only necessary for WSGI)",
64
    )
65
66
    # Parse arguments
67
    args = parser.parse_args(args=args)
68
69
    if args.project is None:
70
        args.project = vcs.find_root(cwd)
71
72 1
    # Configure logging
73 1
    logging.basicConfig(
74 1
        format=settings.VERBOSE_LOGGING_FORMAT, level=settings.VERBOSE_LOGGING_LEVEL
75 1
    )
76 1
77 1
    # Run the program
78 1
    run(args, os.getcwd(), parser.error)
79 1
80
81
def run(args, cwd, _):
82
    """Start the server.
83 1
84
    :param args: Namespace of CLI arguments (from this module or the CLI)
85
    :param cwd: current working directory
86
    :param error: function to call for CLI errors
87
88
    """
89 1
    global tree  # pylint: disable=W0603
90
    tree = build(cwd=cwd, root=args.project)
91
    tree.load()
92 1
    host = args.host
93 1
    port = args.port or settings.SERVER_PORT
94 1
    bottle.TEMPLATE_PATH.insert(
95
        0, os.path.join(os.path.dirname(__file__), '..', 'views')
96
    )
97 1
98
    # If you started without WSGI, the base will be '/'.
99
    if args.baseurl == '' and not args.wsgi:
100 1
        args.baseurl = '/'
101 1
102 1
    # If you specified a base URL, make sure it ends with '/'.
103 1
    if args.baseurl != '' and not args.baseurl.endswith('/'):
104
        args.baseurl += '/'
105 1
106
    bottle.SimpleTemplate.defaults['baseurl'] = args.baseurl
107
    bottle.SimpleTemplate.defaults['navigation'] = True
108 1
109
    if args.launch:
110
        url = utilities.build_url(host=host, port=port)
111 1
        webbrowser.open(url)
112 1
    if not args.wsgi:
113 1
        bottle.run(app=app, host=host, port=port, debug=args.debug, reloader=args.debug)
114
115 1
116 1
@hook('before_request')
117
def strip_path():
118
    request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('/')
119 1
    request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('.html')
120
121
122 1
@hook('after_request')
123 1
def enable_cors():
124 1
    """Allow a webserver running on the same machine to access data."""
125 1
    response.headers['Access-Control-Allow-Origin'] = '*'
126
127 1
128
@get('/')
129
def index():
130 1
    """Read the tree."""
131
    yield template('index', tree_code=tree.draw(html_links=True))
132
133 1
134 1
@get('/documents')
135 1
def get_documents():
136 1
    """Read the tree's documents."""
137 1
    prefixes = [str(document.prefix) for document in tree]
138
    if utilities.json_response(request):
139 1
        data = {'prefixes': prefixes}
140
        return data
141
    else:
142 1
        return template('document_list', prefixes=prefixes)
143
144
145 1
@get('/documents/all')
146 1
def get_all_documents():
147 1
    """Read the tree's documents."""
148 1
    if utilities.json_response(request):
149
        data = {str(d.prefix): {str(i.uid): i.data for i in d} for d in tree}
150 1
        return data
151
    else:
152
        prefixes = [str(document.prefix) for document in tree]
153 1
        return template('document_list', prefixes=prefixes)
154
155
156 1
@get('/documents/<prefix>')
157 1
def get_document(prefix):
158 1
    """Read a tree's document."""
159 1
    document = tree.find_document(prefix)
160 1
    if utilities.json_response(request):
161 1
        data = {str(item.uid): item.data for item in document}
162
        return data
163 1
    else:
164
        return publisher.publish_lines(document, ext='.html', linkify=True)
165
166 1
167
@get('/documents/<prefix>/items')
168
def get_items(prefix):
169 1
    """Read a document's items."""
170 1
    document = tree.find_document(prefix)
171 1
    uids = [str(item.uid) for item in document]
172 1
    if utilities.json_response(request):
173 1
        data = {'uids': uids}
174 1
        return data
175
    else:
176 1
        return template('item_list', prefix=prefix, items=uids)
177 1
178 1
179 1
@get('/documents/<prefix>/items/<uid>')
180 1
def get_item(prefix, uid):
181 1
    """Read a document's item."""
182
    document = tree.find_document(prefix)
183
    item = document.find_item(uid)
184 1
    if utilities.json_response(request):
185
        return {'data': item.data}
186
    else:
187 1
        return publisher.publish_lines(item, ext='.html')
188 1
189 1
190 1
@get('/documents/<prefix>/items/<uid>/attrs')
191 1
def get_attrs(prefix, uid):
192 1
    """Read an item's attributes."""
193
    document = tree.find_document(prefix)
194 1
    item = document.find_item(uid)
195
    attrs = sorted(item.data.keys())
196
    if utilities.json_response(request):
197
        data = {'attrs': attrs}
198
        return data
199
    else:
200
        return '<br>'.join(attrs)
201
202
203
@get('/documents/<prefix>/items/<uid>/attrs/<name>')
204
def get_attr(prefix, uid, name):
205
    """Read an item's attribute value."""
206
    document = tree.find_document(prefix)
207
    item = document.find_item(uid)
208
    value = item.data.get(name, None)
209
    if utilities.json_response(request):
210
        data = {'value': value}
211
        return data
212
    else:
213
        if isinstance(value, str):
214
            return value
215
        try:
216
            return '<br>'.join(str(e) for e in value)
217
        except TypeError:
218
            return str(value)
219
220
221
@get('/assets/doorstop/<filename>')
222
def get_assets(filename):
223
    """Serve static files. Mainly used to serve CSS files and javascript."""
224
    public_dir = os.path.join(
225
        os.path.dirname(__file__), '..', 'core', 'files', 'assets', 'doorstop'
226
    )
227
    return bottle.static_file(filename, root=public_dir)
228
229
230
@post('/documents/<prefix>/numbers')
231
def post_numbers(prefix):
232
    """Create the next number in a document."""
233
    document = tree.find_document(prefix)
234
    number = max(document.next_number, numbers[prefix])
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable numbers does not seem to be defined.
Loading history...
235
    numbers[prefix] = number + 1
236
    if utilities.json_response(request):
237
        data = {'next': number}
238
        return data
239
    else:
240
        return str(number)
241
242
243
if __name__ == '__main__':
244
    main()
245