doorstop.server.main   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 31
eloc 155
dl 0
loc 248
ccs 107
cts 107
cp 1
rs 9.92
c 0
b 0
f 0

14 Functions

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