Passed
Push — master ( 9b4b57...27d41b )
by Matt
01:43
created

PyDMXControl.web._routes   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 274
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 55
eloc 192
dl 0
loc 274
rs 6
c 0
b 0
f 0

17 Functions

Rating   Name   Duplication   Size   Complexity  
A fixture() 0 7 2
A channel() 0 13 3
A fixture_channels() 0 5 2
A home() 0 3 1
A fixture_helpers() 0 2 1
A global_intensity() 0 6 3
A timed_event_data() 0 5 2
B intensity() 0 24 6
A stop_timed_event() 0 9 3
A run_timed_event() 0 9 3
B helper() 0 26 5
A color() 0 21 4
A timed_event() 0 5 2
B channel_val() 0 32 7
A fixture_channel_value() 0 4 2
B park() 0 25 6
A callback() 0 9 3

How to fix   Complexity   

Complexity

Complex classes like PyDMXControl.web._routes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
 *  PyDMXControl: A Python 3 module to control DMX using OpenDMX or uDMX.
3
 *                Featuring fixture profiles, built-in effects and a web control panel.
4
 *  <https://github.com/MattIPv4/PyDMXControl/>
5
 *  Copyright (C) 2023 Matt Cowley (MattIPv4) ([email protected])
6
"""
7
8
from re import compile as re_compile  # Regex
9
from typing import List, Union, Tuple, Dict, Callable  # Typing
10
11
from flask import Blueprint, render_template, current_app, redirect, url_for, jsonify  # Flask
12
13
from .. import Colors  # Colors
14
from ..profiles.defaults import Fixture, Vdim  # Fixtures
15
from ..utils.exceptions import ChannelNotFoundException # Exceptions
16
17
routes = Blueprint('', __name__, url_prefix='/')
18
19
20
def fixture_channels(this_fixture: Fixture) -> List[Tuple[str, int, int]]:
21
    chans = [(f['name'], fixture_channel_value(this_fixture, f['name']), fixture_channel_value(this_fixture, f['name'], True)) for f in this_fixture.channels.values()]
22
    if issubclass(type(this_fixture), Vdim):
23
        chans.append(("dimmer", fixture_channel_value(this_fixture, "dimmer"), fixture_channel_value(this_fixture, "dimmer", True)))
24
    return chans
25
26
27
def fixture_channel_value(this_fixture: Fixture, this_channel: Union[str, int], apply_parking: bool = False) -> int:
28
    if issubclass(type(this_fixture), Vdim):
29
        return this_fixture.get_channel_value(this_channel, False, apply_parking)[0]
30
    return this_fixture.get_channel_value(this_channel, apply_parking)[0]
31
32
33
helpers = ["on", "off", "locate"]
34
35
36
def fixture_helpers(this_fixture: Fixture) -> Dict[str, Callable]:
37
    return {f: this_fixture.__getattribute__(f) for f in helpers if hasattr(this_fixture, f)}
38
39
40
# Home
41
@routes.route('', methods=['GET'])
42
def home():
43
    return render_template("index.jinja2", helpers=helpers)
44
45
46
# Global Intensity
47
@routes.route('intensity/<int:val>', methods=['GET'])
48
def global_intensity(val: int):
49
    if val < 0 or val > 255:
50
        return jsonify({"error": "Value {} is invalid".format(val)}), 400
51
    current_app.parent.controller.all_dim(val)
52
    return jsonify({"message": "All dimmers updated to {}".format(val)}), 200
53
54
55
# Fixture Home
56
@routes.route('fixture/<int:fid>', methods=['GET'])
57
def fixture(fid: int):
58
    this_fixture = current_app.parent.controller.get_fixture(fid)
59
    if not this_fixture:
60
        return redirect(url_for('.home'))
61
    return render_template("fixture.jinja2", fixture=this_fixture, fixture_channels=fixture_channels,
62
                           fixture_channel_value=fixture_channel_value, colors=Colors, helpers=helpers)
63
64
65
# Fixture Channel
66
@routes.route('fixture/<int:fid>/channel/<int:cid>', methods=['GET'])
67
def channel(fid: int, cid: int):
68
    this_fixture = current_app.parent.controller.get_fixture(fid)
69
    if not this_fixture:
70
        return redirect(url_for('.home'))
71
72
    try:
73
        chan = this_fixture.get_channel_id(cid)
74
    except ChannelNotFoundException:
75
        return redirect(url_for('.fixture', fid=this_fixture.id))
76
77
    this_channel = fixture_channels(this_fixture)[chan]
78
    return render_template("channel.jinja2", fixture=this_fixture, channel=this_channel, cid=chan)
79
80
81
# Fixture Channel Set
82
@routes.route('fixture/<int:fid>/channel/<int:cid>/<int:val>', methods=['GET'])
83
def channel_val(fid: int, cid: int, val: int):
84
    this_fixture = current_app.parent.controller.get_fixture(fid)
85
    if not this_fixture:
86
        return jsonify({"error": "Fixture {} not found".format(fid)}), 404
87
88
    try:
89
        chan = this_fixture.get_channel_id(cid)
90
    except ChannelNotFoundException:
91
        return jsonify({"error": "Channel {} not found".format(cid)}), 404
92
93
    if val < 0 or val > 255:
94
        return jsonify({"error": "Value {} is invalid".format(val)}), 400
95
96
    this_fixture.set_channel(chan, val)
97
    val = fixture_channel_value(this_fixture, chan)
98
    val_parked = fixture_channel_value(this_fixture, chan, True)
99
    data = {
100
        "message": "Channel {} {} updated to {}".format(
101
            this_fixture.start_channel + chan,
102
            this_fixture.channels[this_fixture.start_channel + chan]["name"],
103
            val
104
        ),
105
        "elements": {
106
            "channel-{}-value".format(chan): "{}{}".format(val, " ({})".format(val_parked) if this_fixture.parked else ""),
107
            "value": val,
108
            "slider_value": val
109
        }
110
    }
111
    if chan == this_fixture.get_channel_id("dimmer"):
112
        data["elements"]["intensity_value"] = val
113
    return jsonify(data), 200
114
115
116
# Fixture Color
117
@routes.route('fixture/<int:fid>/color/<string:val>', methods=['GET'])
118
def color(fid: int, val: str):
119
    this_fixture = current_app.parent.controller.get_fixture(fid)
120
    if not this_fixture:
121
        return jsonify({"error": "Fixture {} not found".format(fid)}), 404
122
    pattern = re_compile(r"^\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*(?:[, ]\s*(\d{1,3})\s*)*$")
123
    match = pattern.match(val)
124
    if not match:
125
        return jsonify({"error": "Invalid color {} supplied".format(val)}), 400
126
    this_color = [int(f) for f in match.groups() if f]
127
    this_fixture.color(this_color)
128
    return jsonify({
129
        "message": "Color updated to {}".format(this_color),
130
        "elements": dict(
131
            {
132
                "value": Colors.to_hex(this_fixture.get_color())
133
            },
134
            **{"channel-{}-value".format(i): "{}{}".format(f[1], " ({})".format(f[2]) if this_fixture.parked else "")
135
               for i, f in enumerate(fixture_channels(this_fixture))}
136
        )
137
    }), 200
138
139
140
# Fixture Intensity
141
@routes.route('fixture/<int:fid>/intensity/<int:val>', methods=['GET'])
142
def intensity(fid: int, val: int):
143
    this_fixture = current_app.parent.controller.get_fixture(fid)
144
    if not this_fixture:
145
        return jsonify({"error": "Fixture {} not found".format(fid)}), 404
146
147
    try:
148
        chan = this_fixture.get_channel_id("dimmer")
149
    except ChannelNotFoundException:
150
        return jsonify({"error": "Dimmer channel not found"}), 404
151
152
    if val < 0 or val > 255:
153
        return jsonify({"error": "Value {} is invalid".format(val)}), 400
154
155
    this_fixture.set_channel(chan, val)
156
    val = fixture_channel_value(this_fixture, chan)
157
    val_parked = fixture_channel_value(this_fixture, chan, True)
158
    return jsonify({
159
        "message": "Dimmer updated to {}".format(val),
160
        "elements": {
161
            "channel-{}-value".format(chan): "{}{}".format(val, " ({})".format(val_parked) if this_fixture.parked else ""),
162
            "intensity_value": val
163
        }
164
    }), 200
165
166
167
# Fixture Helpers
168
@routes.route('fixture/<int:fid>/helper/<string:val>', methods=['GET'])
169
def helper(fid: int, val: str):
170
    this_fixture = current_app.parent.controller.get_fixture(fid)
171
    if not this_fixture:
172
        return jsonify({"error": "Fixture {} not found".format(fid)}), 404
173
174
    val = val.lower()
175
    this_helpers = fixture_helpers(this_fixture)
176
    if val not in this_helpers.keys():
177
        return jsonify({"error": "Helper {} not found".format(val)}), 404
178
179
    try:
180
        this_helpers[val]()
181
    except Exception:
182
        return jsonify({"error": "Helper {} failed to execute".format(val)}), 500
183
    return jsonify({
184
        "message": "Helper {} executed".format(val),
185
        "elements": dict(
186
            {
187
                "value": Colors.to_hex(this_fixture.get_color()),
188
                "intensity_value": fixture_channel_value(this_fixture, "dimmer")
189
            },
190
            **{"channel-{}-value".format(i): "{}{}".format(f[1], " ({})".format(f[2]) if this_fixture.parked else "")
191
               for i, f in enumerate(fixture_channels(this_fixture))}
192
        )
193
    }), 200
194
195
196
# Fixture Parking
197
@routes.route('fixture/<int:fid>/park', methods=['GET'])
198
def park(fid: int):
199
    this_fixture = current_app.parent.controller.get_fixture(fid)
200
    if not this_fixture:
201
        return jsonify({"error": "Fixture {} not found".format(fid)}), 404
202
203
    state = this_fixture.parked
204
    if state:
205
        this_fixture.unpark()
206
    else:
207
        this_fixture.park()
208
    state = not state
209
210
    return jsonify({
211
        "message": "Fixture {}".format("parked" if state else "unparked"),
212
        "elements": dict(
213
            {
214
                "parking": "Unpark" if state else "Park",
215
                "value": Colors.to_hex(this_fixture.get_color()),
216
                "intensity_value": fixture_channel_value(this_fixture, "dimmer")
217
            },
218
            **{"channel-{}-value".format(i): "{}{}".format(f[1], " ({})".format(f[2]) if this_fixture.parked else "")
219
               for i, f in enumerate(fixture_channels(this_fixture))}
220
        )
221
    }), 200
222
223
224
# Callbacks
225
@routes.route('callback/<string:cb>', methods=['GET'])
226
def callback(cb: str):
227
    if cb not in current_app.parent.callbacks.keys():
228
        return jsonify({"error": "Callback {} not found".format(cb)}), 404
229
    try:
230
        current_app.parent.callbacks[cb]()
231
    except Exception:
232
        return jsonify({"error": "Callback {} failed to execute".format(cb)}), 500
233
    return jsonify({"message": "Callback {} executed".format(cb)}), 200
234
235
236
# Timed Events
237
@routes.route('timed_event/<string:te>', methods=['GET'])
238
def timed_event(te: str):
239
    if te not in current_app.parent.timed_events.keys():
240
        return redirect(url_for('.home'))
241
    return render_template("timed_event.jinja2", te=te)
242
243
244
# Timed Events Data
245
@routes.route('timed_event/<string:te>/data', methods=['GET'])
246
def timed_event_data(te: str):
247
    if te not in current_app.parent.timed_events.keys():
248
        return jsonify({"error": "Timed Event {} not found".format(te)}), 404
249
    return jsonify({"data": current_app.parent.timed_events[te].data}), 200
250
251
252
# Timed Events Run
253
@routes.route('timed_event/<string:te>/run', methods=['GET'])
254
def run_timed_event(te: str):
255
    if te not in current_app.parent.timed_events.keys():
256
        return jsonify({"error": "Timed Event {} not found".format(te)}), 404
257
    try:
258
        current_app.parent.timed_events[te].run()
259
    except Exception:
260
        return jsonify({"error": "Timed Event {} failed to fire".format(te)}), 500
261
    return jsonify({"message": "Timed Event {} fired".format(te), "elements": {te + "-state": "Running"}}), 200
262
263
264
# Timed Events Stop
265
@routes.route('timed_event/<string:te>/stop', methods=['GET'])
266
def stop_timed_event(te: str):
267
    if te not in current_app.parent.timed_events.keys():
268
        return jsonify({"error": "Timed Event {} not found".format(te)}), 404
269
    try:
270
        current_app.parent.timed_events[te].stop()
271
    except Exception:
272
        return jsonify({"error": "Timed Event {} failed to stop".format(te)}), 500
273
    return jsonify({"message": "Timed Event {} stopped".format(te), "elements": {te + "-state": "Stopped"}}), 200
274