Exemplo n.º 1
0
def do_macro(parser, token):
    """ the function taking the parsed tag and returning
    a DefineMacroNode object.
    """
    try:
        bits = token.split_contents()
        tag_name, macro_name, arguments = bits[0], bits[1], bits[2:]
    except IndexError:
        raise template.TemplateSyntaxError(
            "'{0}' tag requires at least one argument (macro name)".format(
                token.contents.split()[0]))

    # use regex's to parse the arguments into arg
    # and kwarg definitions

    # the regex for identifying python variable names is:
    #  r'^[A-Za-z_][\w_]*$'

    # args must be proper python variable names
    # we'll want to capture it from the regex also.
    arg_regex = r'^([A-Za-z_][\w_]*)$'

    # kwargs must be proper variable names with a
    # default value, name="value", or name=value if
    # value is a template variable (potentially with
    # filters).
    # we'll want to capture the name and value from
    # the regex as well.
    kwarg_regex = (
        r'^([A-Za-z_][\w_]*)=(".*"|{0}.*{0}|[A-Za-z_][\w_]*)$'.format("'"))
    # leave further validation to the template variable class

    args = []
    kwargs = {}
    for argument in arguments:
        arg_match = regex_match(arg_regex, argument)
        if arg_match:
            args.append(arg_match.groups()[0])
        else:
            kwarg_match = regex_match(kwarg_regex, argument)
            if kwarg_match:
                kwargs[kwarg_match.groups()[0]] = template.Variable(
                    # convert to a template variable here
                    kwarg_match.groups()[1])
            else:
                raise template.TemplateSyntaxError(
                    "Malformed arguments to the {0} tag.".format(tag_name))

    # parse to the endmacro tag and get the contents
    nodelist = parser.parse(('endmacro', ))
    parser.delete_first_token()

    # store macro in parser._macros, creating attribute
    # if necessary
    _setup_macros_dict(parser)
    parser._macros[macro_name] = DefineMacroNode(macro_name, nodelist, args,
                                                 kwargs)
    return parser._macros[macro_name]
Exemplo n.º 2
0
def get_days_list(from_date, to_date):
    """Returns a list of days included in the specified from-to period"""
    assert regex_match(
        r'^\d\d\d\d\-\d\d-\d\d$',
        from_date), "%s doesn't match format YYYY-MM-DD" % from_date
    assert regex_match(r'^\d\d\d\d\-\d\d-\d\d$',
                       to_date), "%s doesn't match format YYYY-MM-DD" % to_date
    days = []
    from_date = datetime.strptime(from_date, "%Y-%m-%d").date()
    to_date = datetime.strptime(to_date, "%Y-%m-%d").date()
    oneday = timedelta(days=1)
    while from_date < to_date:
        from_date += oneday
        days.append(from_date)
    return days
Exemplo n.º 3
0
def do_usemacro(parser, token):
    """ The function taking a parsed template tag
    and returning a UseMacroNode.
    """
    try:
        bits = token.split_contents()
        tag_name, macro_name, values = bits[0], bits[1], bits[2:]
    except IndexError:
        raise template.TemplateSyntaxError(
            "{0} tag requires at least one argument (macro name)".format(
                token.contents.split()[0]))
    try:
        macro = parser._macros[macro_name]
    except (AttributeError, KeyError):
        raise template.TemplateSyntaxError(
            "Macro '{0}' is not defined previously to the {1} tag".format(macro_name, tag_name))

    args = []
    kwargs = {}

    # leaving most validation up to the template.Variable
    # class, but use regex here so that validation could
    # be added in future if necessary.
    kwarg_regex = (
        r'^([A-Za-z_][\w_]*)=(".*"|{0}.*{0}|[A-Za-z_][\w_]*)$'.format(
            "'"))
    arg_regex = r'^([A-Za-z_][\w_]*|".*"|{0}.*{0}|(\d+))$'.format(
        "'")
    for value in values:
        # must check against the kwarg regex first
        # because the arg regex matches everything!
        kwarg_match = regex_match(
            kwarg_regex, value)
        if kwarg_match:
            kwargs[kwarg_match.groups()[0]] = template.Variable(
                # convert to a template variable here
                kwarg_match.groups()[1])
        else:
            arg_match = regex_match(
                arg_regex, value)
            if arg_match:
                args.append(template.Variable(arg_match.groups()[0]))
            else:
                raise template.TemplateSyntaxError(
                    "Malformed arguments to the {0} tag.".format(
                        tag_name))
    macro.parser = parser
    return UseMacroNode(macro, args, kwargs)
def what_to_say(bot, source, request, private):
    if private:
        check_word = regex_match("is this a word: (?P<word>\w+)", request)
        if check_word:
            return ["Is {} a real word? {}"
                    .format(
                        check_word.group(1),
                        str(is_it_real_word(check_word.group(1)))
                    )]
    if source == 'hubot':
        return what_to_say_hangman(bot, source, request, private)
    elif source == scrabble_bot_name or source == bot.owner:
        global current_letters
        if request.startswith('letters are now '):
            print bot.nickname, request
            current_letters = request[len('letters are now '):]
            return []
        elif request == '{}: your turn'.format(bot.nickname):
            bot.message('picture', ['http://10.47.222.75/images/scrabble/current.png'])
            return scrabble_play_move(current_letters)
        elif request == '{}: play scrabble'.format(bot.nickname):
            return ['{}: join'.format(scrabble_bot_name)]
        else:
            return []
    return []
Exemplo n.º 5
0
  def parse_routes(self):

    from re import match as regex_match

    ROUTES = self.routes

    if len(self.params['uri_array']) == 0:

      if 'default' in ROUTES:
        return self.get_route_tuple(ROUTES['default'])
      else:
        return ('home', 'index', [])

    uri_string = self.concat_uri(self.params['uri_array'])

    if uri_string in ROUTES:
      return self.get_route_tuple(ROUTES[uri_string])

    for route in ROUTES:
      r = route.replace(':any','[a-zA-Z0-9_]')

      if regex_match(r, uri_string):
        controller, method, pre_args = self.get_route_tuple(ROUTES[route], False)
        args_start = len(self.split_routes(r))

        if not method:
          method = self.params['uri_array'][args_start]
          args_start += 1

        args = self.params['uri_array'][args_start:]
        return (controller, method, args)

    else:
      return self.get_route_tuple(uri_string)
Exemplo n.º 6
0
    def go(self) -> tuple:
        mapping = {
            'device\\..*': self._device,
            'group\\.(input|output|connection)': self._device,
        }

        if self.path_main == PATH_CORE:
            check = f'{self.path_type}.{self.path_subtype}'
            matched = False

            for match, goto in mapping.items():
                if regex_match(match, check) is not None:
                    matched = True
                    goto()
                    break

            if matched:
                return self.result

            else:
                log('No route matched!', level=5)

        else:
            log(f"Wrong api root-path supplied => should be '{PATH_CORE}'",
                level=4)

        return self.status
def what_to_say_hangman(bot, source, request, private):
    global current_game

    make_a_move = regex_match("The (?P<word_length>\w+) letter word is:", request)
    if make_a_move:
        word_length = int(make_a_move.group(1))
        if not current_game:
            current_game = HangmanGame(word_length)

        current_game.check_length(word_length)

        word_status = request.split(":")[1].strip().split(' ')
        current_game.game_status(word_status)

        return [current_game.next_letter()]

    if "You have no remaining guesses" == request or request.startswith("Congratulations, you still had"):
        current_game = None
        bot.messenger.messenger.wipe(bot.channel)
        return []

    someone_lost_a_letter = regex_match(
        "You already tried (?P<letter>\w+) so let's pretend that never happened, shall we?",
        request
    )
    if someone_lost_a_letter and current_game:
        i_lost_a_letter = current_game.add_played_letter(someone_lost_a_letter.group(1).lower())
        if i_lost_a_letter:
            return ['oops']

    big_reveal = regex_match(
        "The (?P<length>\w+) letter word was: (?P<word>\w+)",
        request
    )
    if big_reveal:
        word = big_reveal.group(2).lower()
        print [word]
        if is_it_real_word(word):
            return []
        else:
            return [
                "hubot: {} is not in SOWPODS; There's no way I could have guessed that"
                .format(word.upper())
            ]

    return []
Exemplo n.º 8
0
def employee_bulk_creation(request):
    """
    Endpoint to create users using email list
    ---
    parameters:
    - name: body
      required: true
      paramType: body
      pytype: employees.serializers.EmployeeCreationListSerializer
    responseMessages:
    - code: 401
      message: Unauthorized. Authentication credentials were not provided. Invalid token.
    - code: 403
      message: Forbidden.
    - code: 404
      message: Not found
    - code: 406
      message: Request not acceptable
    """
    if request.method == 'POST':
        serializer = EmployeeCreationListSerializer(data=request.data)
        errors = []
        users_created = 0
        if serializer.is_valid():
            email_list = request.data
            for email_object in email_list['emails']:
                email = email_object['email'].lower()
                if regex_match(r"[^@]+@[^@]+\.[^@]+", email):
                    username = email.split('@')[0].lower()
                    domain = email.split('@')[1].lower()
                    if domain in settings.EMAIL_DOMAIN_LIST:
                        if not Employee.objects.filter(email=email).exists():
                            new_employee = Employee.objects.create_user(username,
                                                                        password=request.data['password'],
                                                                        email=email)
                            new_employee.generate_reset_password_code()
                            new_employee.save()
                            users_created += 1
                        else:
                            errors.append(config.USER_EMAIL_ALREADY_REGISTERED % (email))
                    else:
                        errors.append(config.EMAIL_DOMAIN_FORBIDDEN % (domain))
                else:
                    errors.append(config.INVALID_EMAIL_ADDRESS % (email))
        else:
            errors.append(serializer.errors)

        if len(errors) == 0:
            content = {'detail': config.USER_SUCCESSFULLY_CREATED}
            return Response(content, status=status.HTTP_201_CREATED)
        else:
            users_result = {"user_created": users_created}
            detail = {'detail': errors}
            content = users_result.copy()
            content.update(detail)
            return Response(content, status=status.HTTP_406_NOT_ACCEPTABLE)
Exemplo n.º 9
0
def employee_creation(request):
    """
    This endpoint creates a new user with provided email @belatrixsf.com
    ---
    parameters:
    - name: email
      required: true
      paramType: string
      pytype: employees.serializers.EmployeeCreationSerializer
    responseMessages:
    - code: 401
      message: Unauthorized. Authentication credentials were not provided. Invalid token.
    - code: 403
      message: Forbidden.
    - code: 404
      message: Not found
    - code: 406
      message: Request not acceptable
    """
    if request.method == 'POST':
        email = request.data['email']

        if regex_match(r"[^@]+@[^@]+\.[^@]+", email):
            username = email.split('@')[0]
            domain = email.split('@')[1]
        else:
            content = {'detail': config.INVALID_EMAIL_ADDRESS}
            return Response(content, status=status.HTTP_401_UNAUTHORIZED)

        if domain in settings.EMAIL_DOMAIN_LIST:
            random_password = Employee.objects.make_random_password()
            subject = config.EMPLOYEE_CREATION_SUBJECT
            message = config.EMPLOYEE_CREATION_MESSAGE % (username, random_password)

            try:
                new_employee = Employee.objects.create_user(username, password=random_password, email=email)
                new_employee.generate_reset_password_code()
                new_employee.save()
            except Exception as e:
                print e
                content = {'detail': config.USER_EMAIL_ALREADY_REGISTERED}
                return Response(content, status=status.HTTP_406_NOT_ACCEPTABLE)

            try:
                send_email = EmailMessage(subject, message, to=[email])
                send_email.send()
            except Exception as e:
                print e
                content = {'detail': config.USER_SUCCESSFULLY_CREATED_EMAIL_ERROR}
                return Response(content, status=status.HTTP_406_NOT_ACCEPTABLE)

            content = {'detail': config.USER_SUCCESSFULLY_CREATED}
            return Response(content, status=status.HTTP_201_CREATED)
        else:
            content = {'detail': config.EMAIL_DOMAIN_FORBIDDEN % (domain)}
            return Response(content, status=status.HTTP_401_UNAUTHORIZED)
Exemplo n.º 10
0
def parse_macro_params(token):
    """
    Common parsing logic for both use_macro and macro_block
    """
    try:
        bits = token.split_contents()
        tag_name, macro_name, values = bits[0], bits[1], bits[2:]
    except IndexError:
        raise template.TemplateSyntaxError(
            "{0} tag requires at least one argument (macro name)".format(
                token.contents.split()[0]))

    args = []
    kwargs = {}

    # leaving most validation up to the template.Variable
    # class, but use regex here so that validation could
    # be added in future if necessary.
    kwarg_regex = (
        r'^([A-Za-z_][\w_]*)=(".*"|{0}.*{0}|[A-Za-z_][\w_]*)$'.format(
            "'"))
    arg_regex = r'^([A-Za-z_][\w_]*|".*"|{0}.*{0}|(\d+))$'.format(
        "'")
    for value in values:
        # must check against the kwarg regex first
        # because the arg regex matches everything!
        kwarg_match = regex_match(
            kwarg_regex, value)
        if kwarg_match:
            kwargs[kwarg_match.groups()[0]] = template.Variable(
                # convert to a template variable here
                kwarg_match.groups()[1])
        else:
            arg_match = regex_match(
                arg_regex, value)
            if arg_match:
                args.append(template.Variable(arg_match.groups()[0]))
            else:
                raise template.TemplateSyntaxError(
                    "Malformed arguments to the {0} tag.".format(
                        tag_name))

    return tag_name, macro_name, args, kwargs
Exemplo n.º 11
0
def parse_program():
    program = []
    with open('input') as file:
        for line in file:
            if line.startswith('mask'):
                program.append(('mask', line.split('=')[1].strip()))
            else:
                match = regex_match('mem\\[([0-9]+)] = ([0-9]+)', line)
                program.append((int(match.group(1)), int(match.group(2))))
    return program
    def request_from_battleships(self, bot, text):
        challenge = regex_match(
            "(?P<challenger>\w+) has challenged {}! Waiting for accept...".format(bot.nickname), text
        )
        if challenge:
            self.current_game = BattleshipsGame(challenge.group("challenger"), self.grid_size)
            bot.public(["{}: accept".format(battleships_bot)])
            sleep(3)
            bot.message(battleships_bot, generate_random_boat_positions(self.grid_size))
            return ["\o/"]

        if hasattr(self, "current_game"):
            opponent = self.current_game.opponent
            # Responses that imply opponent made a move or it's my turn
            move_texts = [
                "Game is starting between {} and {}".format(opponent, bot.nickname),
                "{}: You already attacked that square! I guess you don't want a turn.".format(opponent),
                "{}: You sunk {}'s".format(opponent, bot.nickname),
                "{}: You hit {}'s".format(opponent, bot.nickname),
                "{}: You didn't hit anything.".format(opponent),
            ]
            for move_text in move_texts:
                if text.startswith(move_text):
                    return self.current_game.make_move()

            hit = regex_match("{}: You hit {}'s (?P<boat_name>\w+).".format(bot.nickname, opponent), text)
            if hit:
                self.current_game.last_move_was_hit(hit.group("boat_name"))

            sink = regex_match("{}: You sunk {}'s (?P<boat_name>\w+).".format(bot.nickname, opponent), text)
            if sink:
                self.current_game.last_move_was_sink(sink.group("boat_name"))

            if text == "{}: You didn't hit anything.".format(bot.nickname):
                self.current_game.last_move_was_miss()

            # This shouldn't come up (unless perhaps the battleships bot crashes?).
            if text == "{}: You're not in a game.".format(bot.nickname):
                del self.current_game
                return ["Oh. :("]
        return []
Exemplo n.º 13
0
def employee_bulk_creation(request):
    """
    Endpoint to create users using email list
    ---
    parameters:
    - name: body
      required: true
      paramType: body
      pytype: employees.serializers.EmployeeCreationListSerializer
    responseMessages:
    - code: 401
      message: Unauthorized. Authentication credentials were not provided. Invalid token.
    - code: 403
      message: Forbidden.
    - code: 404
      message: Not found
    - code: 406
      message: Request not acceptable
    """
    if request.method == 'POST':
        serializer = EmployeeCreationListSerializer(data=request.data)
        errors = []
        users_created = 0
        if serializer.is_valid():
            email_list = request.data
            for email in email_list['emails']:
                if regex_match(r"[^@]+@[^@]+\.[^@]+", email):
                    username = email.split('@')[0]
                    domain = email.split('@')[1]
                    if domain in settings.EMAIL_DOMAIN_LIST:
                        if not Employee.objects.filter(email=email).exists():
                            new_employee = Employee.objects.create_user(username, password=request.data['password'], email=email)
                            new_employee.generate_reset_password_code()
                            new_employee.save()
                            users_created += 1
                        else:
                            errors.append(config.USER_EMAIL_ALREADY_REGISTERED % (email))
                    else:
                        errors.append(config.EMAIL_DOMAIN_FORBIDDEN % (domain))
                else:
                    errors.append(config.INVALID_EMAIL_ADDRESS % (email))
        else:
            errors.append(serializer.errors)

        if len(errors) == 0:
            content = {'detail': config.USER_SUCCESSFULLY_CREATED}
            return Response(content, status=status.HTTP_201_CREATED)
        else:
            users_result = {"user_created": users_created}
            detail = {'detail': errors}
            content = users_result.copy()
            content.update(detail)
            return Response(content, status=status.HTTP_406_NOT_ACCEPTABLE)
Exemplo n.º 14
0
def load_allowed_controllers():
    # Loads Allowed Ip's (From controllers, added later to ALLOWED_HOSTS in settings)

    ip_pattern = "^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$"
    controllers = []

    with open(ALLOWED_CTRL_DATAFILE, 'r') as file:
        for line in file.readlines():
            reg_line = regex_match(ip_pattern, line)
            if bool(reg_line):
                controllers.append(reg_line.group())

    if controllers:
        ALL_CTRL_ALLOWED.extend(controllers)
        return True

    return False
Exemplo n.º 15
0
def user_creation(request):
    """
    Create user account
    ---
    POST:
        serializer: participants.serializers.UserCreationSerializer
        response_serializer: participants.serializers.UserSerializer
    """
    if request.method == 'POST':
        serializer = UserCreationSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            email = str(serializer.validated_data['email']).lower()

        if not regex_match(r"[^@]+@[^@]+\.[^@]+", email):
            raise ParseError("Correo invalido.")

        random_password = User.objects.make_random_password(length=4, allowed_chars='hacktrx23456789')
        subject = "[Hackatrix] Usuario creado para la Hackatrix"
        message = "Su clave temporal, que debe cambiar es: %s" % (random_password)

        try:
            new_user = User.objects.create_user(email, password=random_password)
            new_user.save()
        except Exception as e:
            print(e)
            raise NotAcceptable('Correo ya registrado.')

        participant = Participant.objects.filter(email=new_user.email)
        if len(participant) == 1:
            event = Event.objects.filter(pk=participant[0].event_id)
            if len(event) == 1:
                new_user.full_name = participant[0].full_name
                new_user.save()
                EventParticipant.objects.create(event=event[0], participant=new_user)

        try:
            send_email = EmailMessage(subject, message, to=[email])
            send_email.send()
        except Exception as e:
            print(e)
            content = {'detail: Problemas con el envio de correo electronico'}
            return Response(content, status=status.HTTP_503_SERVICE_UNAVAILABLE)

        serializer = UserSerializer(new_user)
        return Response(serializer.data, status=status.HTTP_201_CREATED)
Exemplo n.º 16
0
def parse_input():
    fields = {}
    nearby_tickets = []
    with open('input') as file:
        # parse rules
        while (line := file.readline().strip()) != '':
            match = regex_match(
                '([a-z ]+): ([0-9]+-[0-9]+) or ([0-9]+-[0-9]+)', line)
            start1, end1 = match.group(2).split('-')
            start2, end2 = match.group(3).split('-')
            fields[match.group(1)] = (range(int(start1),
                                            int(end1) + 1),
                                      range(int(start2),
                                            int(end2) + 1))

        # parse own ticket
        file.readline()
        ticket = [int(value) for value in file.readline().strip().split(',')]

        # parse nearby tickets
        file.readline()
        file.readline()
        while (line := file.readline().strip()) != '':
            nearby_tickets.append([int(value) for value in line.split(',')])
Exemplo n.º 17
0
def what_to_say(bot, source, text, private):
    global bot_messenger
    global bot_channel
    global current_game
    global last_game_time
    bot_messenger = bot.message
    bot_channel = bot.channel
    if private:
        if 'wipe game' in text:
            current_game = None
    else:
        if text.startswith("{}: ".format(bot.nickname)):
            command = text[len("{}: ".format(bot.nickname)):]
            
            if command == 'help':
                return [
                    'Love Letter',
                    'Full rules here http://www.alderac.com/tempest/files/2012/09/Love_Letter_Rules_Final.pdf',
                    'Commands are: "First to [N]" to start a new game, "join", "start" when enough players have joined',
                    '"status" and "scores" to find out about the game in progress',
                    '"card values" to be reminded of the values of each card',
                    'When choosing which card to play or guess, just give the numeric value of that card']

            if command == 'card values':
                return [
                    'value - name (frequency): description',
                    '8 - Princess (1): Lose if discarded',
                    '7 - Countess (1): Must discard if caught with King or Prince',
                    '6 - King (1): Trade hands',
                    '5 - Prince (2): One player must discard their hand',
                    '4 - Handmaid (2): Protection until your next turn',
                    '3 - Baron (2): Compare hands; lower hand is out',
                    '2 - Priest (2): Look at a hand',
                    "1 - Guard (5): Guess a player's hand",
                ]
            new_game = regex_match("first to ([0-9]{1})", command)
            if new_game:
                if current_game:
                    return ["{}: There's already a game being played"
                            .format(source)]
                else:
                    current_game = LoveLetterGame(
                        messenger, 
                        int(new_game.groups(0)[0]),
                        source
                    )
            else:
                if not current_game:
                    return ["{}: Start a game with 'first to N'"
                            .format(source)]
                else:
                    if command == 'start':
                        if time() < last_game_time + 3600:
                            return ["You played already in the last hour."]
                        current_game.start_game(source)
                    elif command == 'join':
                        current_game.add_player(source)
                    elif current_game.game_started:
                        if command == 'status':
                            current_game.status()
                        elif command == 'scores':
                            current_game.show_scores()
                        elif command == 'next round':
                            current_game.new_round(source)
                        else:
                            outcome = current_game.make_move(source, command)
                            if outcome == 'game over':
                                current_game = None
    return []
 def isMathExpression(string):
     return regex_match(
         r'[123456789]\d*((\+|\-|x|X|\*)[123456789]\d*)*=?([123456789]\d*)?',
         string)
 def isOrderedNumber(string):
     return regex_match(r'[123456789]\d*(th|rd|nd|st)', string)
 def isScientificNotation(string):
     return regex_match(
         r'([123456789]\d*(\.\d*)?([xX\*]))?(\-)?10\^([-+])?\d+', string)
Exemplo n.º 21
0
def format_file_size(size: str) -> str:
    """Returns file size in a formatted string (i.e. 706 MiB)"""
    return "{} {}".format(
        *regex_match(r"^.*?(?=(\d+.?\d*)).+?(?:(B|KiB|MiB|GiB|MB|GB))", size).groups()
    )
 def isExponent(string):
     return regex_match(r'[123456789]\d*^([\-\+])?\d+', string)
Exemplo n.º 23
0
	def __init__(self, window):
		super().__init__()
		self.setupUi(self)
		
		# API init.
		self.control = api.control()
		self.video = api.video()

		# Panel init.
		self.setFixedSize(window.app.primaryScreen().virtualSize()) #This is not a responsive design.
		self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
		self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
		
		self._window = window
		
		#Hide the fake borders if the buttons don't have borders.
		if self.uiBlackCal.hideBorder:
			self.uiBottomHorizontalLine.hide()
			self.uiBottomVerticalLine.hide()
		else:
			self.uiBottomHorizontalLine.show()
			self.uiBottomVerticalLine.show()
		
		self.uiFocusPeakingOriginalCustomStyleSheet = self.uiFocusPeaking.styleSheet()
		self.uiZebraStripesOriginalCustomStyleSheet = self.uiZebraStripes.styleSheet()
		settings.observe('theme', 'dark', lambda name: (
			self.uiFocusPeaking.setStyleSheet(
				self.uiFocusPeakingOriginalCustomStyleSheet + f"""
					CheckBox {{ background-color: {theme(name).background} }}
				"""
			),
			self.uiZebraStripes.setStyleSheet(
				self.uiZebraStripesOriginalCustomStyleSheet + f"""
					CheckBox {{ background-color: {theme(name).background} }}
				"""
			)
		))
		
		#Note start/end recording times, to display the timers.
		recordingStartTime = 0
		recordingEndTime = 0
		def updateRecordingStartTime(state):
			nonlocal recordingStartTime, recordingEndTime
			if state == 'recording':
				recordingStartTime = time()
				recordingEndTime = 0
				self.uiPlayAndSave.update()
				self.uiRecord.update()
			elif state == 'idle' and not recordingEndTime:
				recordingEndTime = time()
				self.uiPlayAndSave.update()
				self.uiRecord.update()
		api.observe('state', updateRecordingStartTime)
		
		totalFrames = self.control.getSync('totalFrames')
		if totalFrames == 0: #Set the length of the recording to 0, if nothing has been recorded. Otherwise, calculate what we've recorded.
			recordingStartTime = recordingEndTime
		else:
			recordingEndTime = recordingStartTime + totalFrames/self.control.getSync('frameRate')
		
		self.uiMenuBackground.hide()
		self.uiMenuBackground.move(0,0)
		
		lastOpenerButton = None
		
		def showMenu(button: QWidget, menu: QWidget, *_):
			nonlocal lastOpenerButton
			lastOpenerButton = button
			
			self.uiMenuBackground.show(),
			self.uiMenuBackground.raise_(),
			button.raise_(),
			menu.show(),
			menu.raise_(),
			self.focusRing.raise_(),
		self.showMenu = showMenu
			
		def hideMenu(*_):
			nonlocal lastOpenerButton
			if not lastOpenerButton:
				return
			
			self.uiMenuBackground.hide()
			self.uiFocusPeakingColorMenu.hide()
			self.uiMenuDropdown.hide()
			self.uiExposureMenu.hide()
			self.uiWhiteBalanceMenu.hide()
			lastOpenerButton.setFocus()
			
			lastOpenerButton = None
		self.hideMenu = hideMenu
		
		self.uiMenuBackground.mousePressEvent = hideMenu
		self.uiMenuBackground.focusInEvent = hideMenu
		
		
		#############################
		#   Button action binding   #
		#############################
		
		#Debug buttons. (These are toggled on the factory settings screen.)
		self.uiDebugA.clicked.connect(self.screenshotAllScreens)
		self.uiDebugB.clicked.connect(lambda: window.show('test'))
		self.uiDebugC.setFocusPolicy(QtCore.Qt.TabFocus) #Break into debugger without loosing focus, so you can debug focus issues.
		self.uiDebugC.clicked.connect(lambda: self and window and dbg()) #"self" is needed here, won't be available otherwise.
		self.uiDebugD.clicked.connect(QApplication.closeAllWindows)
		
		#Only show the debug controls if enabled in factory settings.
		settings.observe('debug controls enabled', False, lambda show: (
			self.uiDebugA.show() if show else self.uiDebugA.hide(),
			self.uiDebugB.show() if show else self.uiDebugB.hide(),
			self.uiDebugC.show() if show else self.uiDebugC.hide(),
			self.uiDebugD.show() if show else self.uiDebugD.hide(),
		))
		
		
		#Occasionally, the touch screen seems to report a spurious touch event on the top-right corner. This should prevent that. (Since the record button is there now, this is actually very important!)
		self.uiErrantClickCatcher.mousePressEvent = (lambda evt:
			log.warn('Errant click blocked. [WpeWCY]'))
		
		
		#Zeebs
		api.observe('zebraLevel', lambda intensity:
			self.uiZebraStripes.setCheckState(
				0 if not intensity else 2 ) )
		
		self.uiZebraStripes.stateChanged.connect(lambda state: 
			self.control.set({'zebraLevel': state/200}) )
		
		
		#Focus peaking
		#Use for focus peaking drop-down.
		#api.observe('focusPeakingLevel', lambda intensity:
		#	self.uiFocusPeakingIntensity.setCurrentIndex(
		#		round((1-intensity) * (self.uiFocusPeakingIntensity.count()-1)) ) )
		#
		#self.uiFocusPeakingIntensity.currentIndexChanged.connect(lambda index:
		#	self.control.set({'focusPeakingLevel': 1-(index/(self.uiFocusPeakingIntensity.count()-1))} ) )
		
		api.observe('focusPeakingLevel', lambda intensity:
			self.uiFocusPeaking.setCheckState(
				0 if not intensity else 2 ) )
		
		self.uiFocusPeaking.stateChanged.connect(lambda state: 
			self.control.set({'focusPeakingLevel': (state/2) * 0.2}) )
		
		
		#Focus peaking colour
		focusColor = ''
		def updateFocusColor(color):
			nonlocal focusColor
			target = getattr(self, f"ui{color.title()}FocusPeaking", None)
			if target: #Find the colour of the panel to be highlighted.
				match = regex_search(r'background:\s*?([#\w]+)', target.customStyleSheet)
				assert match, f"Could not find background color of {target.objectName()}. Check the background property of it's customStyleSheet."
				focusColor = match.group(1)
			else: #Just pass through whatever the colour is.
				focusColor = color
			
			self.uiFocusPeakingColor.update()
		api.observe('focusPeakingColor', updateFocusColor)
			
		def uiFocusPeakingColorPaintEvent(evt, rectSize=24):
			"""Draw the little coloured square on the focus peaking button."""
			midpoint = self.uiFocusPeakingColor.geometry().size()/2 + QSize(0, self.uiFocusPeakingColor.touchMargins()['top']/2)
			type(self.uiFocusPeakingColor).paintEvent(self.uiFocusPeakingColor, evt) #Invoke the superclass to - hopefully - paint the rest of the button before we deface it with our square.
			p = QPainter(self.uiFocusPeakingColor)
			p.setPen(QPen(QColor('black')))
			p.setBrush(QBrush(QColor(focusColor)))
			p.drawRect( #xywh
				midpoint.width() - rectSize/2, midpoint.height() - rectSize/2,
				rectSize, rectSize )
		self.uiFocusPeakingColor.paintEvent = uiFocusPeakingColorPaintEvent
		
		self.uiFocusPeakingColor.clicked.connect(
			self.toggleFocusPeakingColorMenu)
		
		self.uiFocusPeakingColorMenu.hide()
		self.uiFocusPeakingColorMenu.move(360, 330)
		
		
		#Loop focus peaking color menu focus, for the jog wheel.
		self.uiMagentaFocusPeaking.nextInFocusChain = (lambda *_: 
			self.uiFocusPeakingColor
			if self.uiFocusPeakingColorMenu.isVisible() else
			type(self.uiMagentaFocusPeaking).nextInFocusChain(self.uiMagentaFocusPeaking, *_)
		)
		self.uiRedFocusPeaking.previousInFocusChain = (lambda *_: 
			self.uiFocusPeakingColor
			if self.uiFocusPeakingColorMenu.isVisible() else
			type(self.uiRedFocusPeaking).previousInFocusChain(self.uiRedFocusPeaking, *_)
		)
		self.uiFocusPeakingColor.nextInFocusChain = (lambda *_:
			self.uiRedFocusPeaking
			if self.uiFocusPeakingColorMenu.isVisible() else
			type(self.uiFocusPeakingColor).nextInFocusChain(self.uiFocusPeakingColor, *_)
		)
		self.uiFocusPeakingColor.previousInFocusChain = (lambda *_:
			self.uiMagentaFocusPeaking
			if self.uiFocusPeakingColorMenu.isVisible() else
			type(self.uiFocusPeakingColor).previousInFocusChain(self.uiFocusPeakingColor, *_)
		)
		
		#Focus peaking color menu
		api.observe('focusPeakingColor', self.updateFocusPeakingColor)
		
		for child in self.uiFocusPeakingColorMenu.children():
			match = regex_match(r'^ui(.*?)FocusPeaking$', child.objectName())
			match and child.clicked.connect(
				(lambda color: #Capture color from for loop.
					lambda: self.control.set({'focusPeakingColor': color})
				)(match.group(1).lower()) )
		
		
		#Black Cal
		self.uiBlackCal.clicked.connect(lambda:
			self.control.call('startCalibration', {
				'blackCal': True ,
				'saveCal':  True }) )
		
		
		#White Bal & Trigger/IO
		whiteBalanceTemplate = self.uiWhiteBalance.text()
		api.observe('wbTemperature', lambda temp:
			self.uiWhiteBalance.setText(
				whiteBalanceTemplate.format(temp) ))
		
		self.uiTriggers.clicked.connect(lambda:
			window.show('triggers_and_io') )
		
		# You can't adjust the colour of a monochromatic image.
		# Hide white balance in favour of trigger/io button.
		if api.apiValues.get('sensorColorPattern') == 'mono':
			self.uiWhiteBalance.hide()
		else:
			self.uiTriggers.hide()
		
		self.uiWhiteBalanceMenu.hide()
		self.uiWhiteBalanceMenu.move(
			self.x(),
			self.uiWhiteBalance.y() - self.uiWhiteBalanceMenu.height() + self.uiWhiteBalance.touchMargins()['top'],
		)
		self.uiWhiteBalance.clicked.connect(lambda *_: 
			hideMenu()
			if self.uiWhiteBalanceMenu.isVisible() else
			showMenu(self.uiWhiteBalance, self.uiWhiteBalanceMenu)
		)
		
		#Loop white balance menu focus, for the jog wheel.
		self.uiFineTuneColor.nextInFocusChain = (lambda *_: 
			self.uiWhiteBalance
			if self.uiWhiteBalanceMenu.isVisible() else
			type(self.uiFineTuneColor).nextInFocusChain(self.uiFineTuneColor, *_)
		)
		self.uiWBPreset1.previousInFocusChain = (lambda *_: 
			self.uiWhiteBalance
			if self.uiWhiteBalanceMenu.isVisible() else
			type(self.uiWBPreset1).previousInFocusChain(self.uiWBPreset1, *_)
		)
		self.uiWhiteBalance.nextInFocusChain = (lambda *_:
			self.uiWBPreset1
			if self.uiWhiteBalanceMenu.isVisible() else
			type(self.uiWhiteBalance).nextInFocusChain(self.uiWhiteBalance, *_)
		)
		self.uiWhiteBalance.previousInFocusChain = (lambda *_:
			self.uiFineTuneColor
			if self.uiWhiteBalanceMenu.isVisible() else
			type(self.uiWhiteBalance).previousInFocusChain(self.uiWhiteBalance, *_)
		)
		
		self.uiWBPreset1.clicked.connect(lambda evt:
			self.control.set('wbTemperature', self.uiWBPreset1.property('temp')) )
		self.uiWBPreset2.clicked.connect(lambda evt:
			self.control.set('wbTemperature', self.uiWBPreset2.property('temp')) )
		self.uiWBPreset3.clicked.connect(lambda evt:
			self.control.set('wbTemperature', self.uiWBPreset3.property('temp')) )
		self.uiWBPreset4.clicked.connect(lambda evt:
			self.control.set('wbTemperature', self.uiWBPreset4.property('temp')) )
		self.uiWBPreset5.clicked.connect(lambda evt:
			self.control.set('wbTemperature', self.uiWBPreset5.property('temp')) )
		self.uiFineTuneColor.clicked.connect(lambda: window.show('color'))
		
		
		#Exposure
		def updateExposureSliderLimits():
			"""Update exposure text to match exposure slider, and sets the slider step so clicking the gutter always moves 1%."""
			step1percent = (self.uiExposureSlider.minimum() + self.uiExposureSlider.maximum()) // 100
			self.uiExposureSlider.setSingleStep(step1percent)
			self.uiExposureSlider.setPageStep(step1percent*10)
		
		def onExposureSliderMoved(newExposureNs):
			nonlocal exposureNs
			
			linearRatio = (newExposureNs-self.uiExposureSlider.minimum()) / (self.uiExposureSlider.maximum()-self.uiExposureSlider.minimum())
			newExposureNs = math.pow(linearRatio, 2) * self.uiExposureSlider.maximum()
			self.control.call('set', {'exposurePeriod': newExposureNs})
			
			#The signal takes too long to return, as it's masked by the new value the slider sets.
			exposureNs = newExposureNs
			updateExposureText()
		
		def updateExposureMax(newExposureNs):
			self.uiExposureSlider.setMaximum(newExposureNs)
			updateExposureSliderLimits()
		
		def updateExposureMin(newExposureNs):
			self.uiExposureSlider.setMinimum(newExposureNs)
			updateExposureSliderLimits()
		
		#Must set slider min/max before value.
		api.observe('exposureMax', updateExposureMax)
		api.observe('exposureMin', updateExposureMin)
		
		exposureUnit = 'µs' #One of 'µs', 'deg', or 'pct'.
		exposureTemplate = self.uiExposure.text()
		uiExposureInDegreesTemplate = self.uiExposureInDegrees.text()
		uiExposureInMsTemplate = self.uiExposureInMs.text()
		uiExposureInPercentTemplate = self.uiExposureInPercent.text()
		
		exposureNsMin = 0
		exposureNs = 0
		exposureNsMax = 0
		
		def updateExposureText(*_):
			exposureDeg = exposureNs/api.apiValues.get('framePeriod')*360
			exposurePct = exposureNs/(exposureNsMax or 1)*100
			exposureMs = exposureNs/1e3
			
			if exposurePct < 0:
				dbg()
			
			self.uiExposure.setText(
				exposureTemplate.format(
					name = {
						'µs': 'Exposure',
						'pct': 'Exposure',
						'deg': 'Shutter Angle',
					}[exposureUnit],
					exposure = {
						'deg': f'{exposureDeg:1.0f}°', #TODO DDR 2019-09-27: Is this actually the way to calculate shutter angle?
						'pct': f'{exposurePct:1.1f}%',
						'µs': f'{exposureMs:1.1f}µs',
					}[exposureUnit],
				)
			)
			
			self.uiExposureInDegrees.setText(
				uiExposureInDegreesTemplate.format(
					degrees = exposureDeg ) )
			
			self.uiExposureInPercent.setText(
				uiExposureInPercentTemplate.format(
					percent = exposurePct ) )
			
			self.uiExposureInMs.setText(
				uiExposureInMsTemplate.format(
					duration = exposureMs ) )
			
			linearRatio = exposureNs/(exposureNsMax-exposureNsMin)
			try:
				exponentialRatio = math.sqrt(linearRatio)
			except ValueError:
				exponentialRatio = 0
			if not self.uiExposureSlider.beingHeld:
				self.uiExposureSlider.setValue(exponentialRatio * (self.uiExposureSlider.maximum()-self.uiExposureSlider.minimum()) + self.uiExposureSlider.minimum())
			updateExposureSliderLimits()
		
		# In Python 3.7: Use api.observe('exposureMin', lambda ns: exposureNSMin := ns) and give exposureNSMin a setter?
		def updateExposureNsMin(ns):
			nonlocal exposureNsMin
			exposureNsMin = ns
			updateExposureText()
		api.observe('exposureMin', updateExposureNsMin)
		
		def updateExposureNs(ns):
			nonlocal exposureNs
			exposureNs = ns
			updateExposureText()
		api.observe('exposurePeriod', updateExposureNs)
		
		def updateExposureNsMax(ns):
			nonlocal exposureNsMax
			exposureNsMax = ns
			updateExposureText()
		api.observe('exposureMax', updateExposureNsMax)
		
		api.observe('framePeriod', updateExposureText)
		
		self.uiExposureMenu.hide()
		self.uiExposureMenu.move(
			self.x(),
			self.uiExposure.y() - self.uiExposureMenu.height() + self.uiExposure.touchMargins()['top'],
		)
		self.uiExposure.clicked.connect(lambda *_: 
			hideMenu()
			if self.uiExposureMenu.isVisible() else
			showMenu(self.uiExposure, self.uiExposureMenu)
		)
		
		#Loop exposure menu focus, for the jog wheel.
		self.uiExposureSlider.nextInFocusChain = (lambda *_: 
			self.uiExposure
			if self.uiExposureMenu.isVisible() else
			type(self.uiExposureSlider).nextInFocusChain(self.uiExposureSlider, *_)
		)
		self.uiExposureInDegrees.previousInFocusChain = (lambda *_: 
			self.uiExposure
			if self.uiExposureMenu.isVisible() else
			type(self.uiExposureInDegrees).previousInFocusChain(self.uiExposureInDegrees, *_)
		)
		self.uiExposure.nextInFocusChain = (lambda *_:
			self.uiExposureInDegrees
			if self.uiExposureMenu.isVisible() else
			type(self.uiExposure).nextInFocusChain(self.uiExposure, *_)
		)
		self.uiExposure.previousInFocusChain = (lambda *_:
			self.uiExposureSlider
			if self.uiExposureMenu.isVisible() else
			type(self.uiExposure).previousInFocusChain(self.uiExposure, *_)
		)
		
		def uiExposureInDegreesClicked(*_):
			nonlocal exposureUnit
			exposureUnit = 'deg'
			updateExposureText()
		self.uiExposureInDegrees.clicked.connect(
			uiExposureInDegreesClicked )
		
		def uiExposureInMsClicked(*_):
			nonlocal exposureUnit
			exposureUnit = 'µs'
			updateExposureText()
		self.uiExposureInMs.clicked.connect(
			uiExposureInMsClicked )
		
		def uiExposureInPercentClicked(*_):
			nonlocal exposureUnit
			exposureUnit = 'pct'
			updateExposureText()
		self.uiExposureInPercent.clicked.connect(
			uiExposureInPercentClicked )
		
		
		#Exposure Slider - copied from the original main.py.
		self.uiExposureSlider.debounce.sliderMoved.connect(onExposureSliderMoved)
		self.uiExposureSlider.touchMargins = lambda: {
			"top": 10, "left": 10, "bottom": 10, "right": 10
		}
		
		
		
		
		
		#Resolution
		resolutionTemplate = self.uiResolution.text()
		
		hRes = 0
		vRes = 0
		fps = 0
		
		def updateResolutionText():
			self.uiResolution.setText(
				resolutionTemplate.format(
					hRes=hRes, vRes=vRes, fps=fps ) )
		
		def updateFps(framePeriodNs):
			nonlocal fps
			fps = 1e9 / framePeriodNs
			updateResolutionText()
		api.observe('framePeriod', updateFps)
		
		def updateResolution(resolution):
			nonlocal hRes, vRes
			hRes = resolution['hRes']
			vRes = resolution['vRes']
			updateResolutionText()
		api.observe('resolution', updateResolution)
		
		self.uiResolution.clicked.connect(lambda:
			window.show('recording_settings') )
		
		
		#Menu
		self.uiMenuDropdown.hide()
		self.uiMenuDropdown.move(
			self.uiMenuButton.x(),
			self.uiMenuButton.y() + self.uiMenuButton.height() -  self.uiMenuButton.touchMargins()['bottom'] - self.uiMenuFilter.touchMargins()['top'] - 1, #-1 to merge margins.
		)
		self.uiMenuButton.clicked.connect((lambda:
			hideMenu() 
			if self.uiMenuDropdown.isVisible() else
			showMenu(self.uiMenuButton, self.uiMenuDropdown)
		))
		
		#Loop main menu focus, for the jog wheel.
		self.uiMenuScroll.nextInFocusChain = (lambda *_: #DDR 2019-10-21: This doesn't work, and seems to break end-of-scroll progression in the menu scroll along the way.
			self.uiMenuButton
			if self.uiMenuDropdown.isVisible() else
			type(self.uiMenuScroll).nextInFocusChain(self.uiMenuScroll, *_)
		)
		self.uiMenuFilter.previousInFocusChain = (lambda *_: 
			self.uiMenuButton
			if self.uiMenuDropdown.isVisible() else
			type(self.uiMenuFilter).previousInFocusChain(self.uiMenuFilter, *_)
		)
		self.uiMenuButton.nextInFocusChain = (lambda *_:
			self.uiMenuFilter
			if self.uiMenuDropdown.isVisible() else
			type(self.uiMenuButton).nextInFocusChain(self.uiMenuButton, *_)
		)
		self.uiMenuButton.previousInFocusChain = (lambda *_:
			self.uiMenuScroll
			if self.uiMenuDropdown.isVisible() else
			type(self.uiMenuButton).previousInFocusChain(self.uiMenuButton, *_)
		)
		
		# Populate uiMenuScroll from actions.
		# Generally, anything which has a button on the main screen will be
		# hidden in this menu, which means it won't come up unless we search
		# for it. This should -- hopefully -- keep the clutter down without
		# being confusing.
		_whiteBalAvail = api.apiValues.get('sensorColorPattern') == 'mono' #Black and white models of the Chronos do not have colour to balance, so don't show that screen ever.
		_scriptsHidden = not [f for f in iglob('/var/camera/scripts/*')][:1] #Only show the scripts screen if there will be a script on it to run.
		log.debug(f'_scriptsHidden {_scriptsHidden}')
		main_menu_items = [
			{'name':"About Camera",          'open':lambda: window.show('about_camera'),          'hidden': False,           'synonyms':"kickstarter thanks name credits"},
			{'name':"App & Internet",        'open':lambda: window.show('remote_access'),         'hidden': False,           'synonyms':"remote access web client network control api"},
			{'name':"Battery & Power",       'open':lambda: window.show('power'),                 'hidden': True,            'synonyms':"charge wake turn off power down"},
			{'name':"Camera Settings",       'open':lambda: window.show('user_settings'),         'hidden': False,           'synonyms':"user operator save settings"},
			{'name':"Custom Scripts",        'open':lambda: window.show('scripts'),               'hidden': _scriptsHidden,  'synonyms':"scripting bash python"},
			{'name':"Factory Utilities",     'open':lambda: window.show('service_screen.locked'), 'hidden': False,           'synonyms':"utils"},
			{'name':"Format Storage",        'open':lambda: window.show('storage'),               'hidden': True,            'synonyms':"file saving save media df mounts mounted devices thumb drive ssd sd card usb stick filesystem reformat"},
			{'name':"Interface Options",     'open':lambda: window.show('primary_settings'),      'hidden': False,           'synonyms':"rotate rotation screen set time set date"},
			{'name':"Play & Save Recording", 'open':lambda: window.show('play_and_save'),         'hidden': True,            'synonyms':"mark region saving"},
			{'name':"Record Mode",           'open':lambda: window.show('record_mode'),           'hidden': False,           'synonyms':"segmented run n gun normal"},
			{'name':"Recording Settings",    'open':lambda: window.show('recording_settings'),    'hidden': True,            'synonyms':"resolution framerate offset gain boost brightness exposure"},
			{'name':"Review Saved Videos",   'open':lambda: window.show('replay'),                'hidden': False,           'synonyms':"playback show footage saved card movie replay"},
			#{'name':"Stamp Overlay",         'open':lambda: window.show('stamp'),                 'hidden': False,           'synonyms':"watermark"},
			#{'name':"Trigger Delay",        'open':lambda: window.show('trigger_delay'),         'hidden': False,           'synonyms':"wait"}, #Removed because we use the trigger/io delay block now.
			{'name':"Triggers & IO",         'open':lambda: window.show('triggers_and_io'),       'hidden': False,           'synonyms':"bnc green ~a1 ~a2 trig1 trig2 trig3 signal input output trigger delay gpio"},
			{'name':"Update Camera",         'open':lambda: window.show('update_firmware'),       'hidden': False,           'synonyms':"firmware"},
			{'name':"Video Save Settings",   'open':lambda: window.show('file_settings'),         'hidden': True,            'synonyms':"file saving"},
		]
		if(_whiteBalAvail):
			main_menu_items += [
				{'name':"Color",             'open':lambda: window.show('white_balance'),         'hidden': True,            'synonyms':"matrix colour white balance temperature"},
			]
		
		
		menuScrollModel = QStandardItemModel(
			len(main_menu_items), 1, self.uiMenuScroll )
		for i in range(len(main_menu_items)):
			menuScrollModel.setItemData(menuScrollModel.index(i, 0), {
				Qt.DisplayRole: main_menu_items[i]['name'],
				Qt.UserRole: main_menu_items[i],
				Qt.DecorationRole: None, #Icon would go here.
			})
		self.uiMenuScroll.setModel(menuScrollModel)
		self.uiMenuScroll.clicked.connect(self.showOptionOnTap)
		self.uiMenuScroll.jogWheelClick.connect(self.showOptionOnJogWheelClick)
		
		self.uiMenuFilterIcon.setAttribute(Qt.WA_TransparentForMouseEvents) #Allow clicking on the filter icon, 🔎, to filter.
		self.uiMenuFilter.textChanged.connect(self.filterMenu)
		self.filterMenu()
		
		self.uiMenuFilterX.clicked.connect(self.uiMenuFilter.clear)
		
		
		#Battery
		self._batteryCharge   = 1
		self._batteryCharging = 0
		self._batteryPresent  = 0
		self._batteryBlink = False
		self._theme = 'light'
		
		self._batteryTemplate = self.uiBattery.text()
		
		self._batteryPollTimer = QtCore.QTimer()
		self._batteryPollTimer.timeout.connect(self.updateBatteryCharge)
		self._batteryPollTimer.setTimerType(QtCore.Qt.VeryCoarseTimer) #Infrequent, wake as little as possible.
		self._batteryPollTimer.setInterval(3600)
		
		self._batteryBlinkTimer = QtCore.QTimer()
		self._batteryBlinkTimer.timeout.connect(lambda: (
			setattr(self, '_batteryBlink', not self._batteryBlink),
			self.updateBatteryIcon(),
		))
		self._batteryBlinkTimer.setInterval(500) #We display percentages. We update in tenth-percentage increments.
		
		self.uiBattery.clicked.connect(lambda: window.show('power'))
		
		self.uiBatteryIcon.setAttribute(Qt.WA_TransparentForMouseEvents)
		self.uiBatteryIcon.setStyleSheet('')
		api.observe('externalPower', lambda state: (
			setattr(self, '_batteryCharging', state),
			state and (
				self._batteryBlinkTimer.stop(),
				setattr(self, '_batteryBlink', False),
			),
			self.updateBatteryIcon(),
		) )
		api.observe('batteryPresent', lambda state: (
			setattr(self, '_batteryPresent', state),
			state and (
				self._batteryBlinkTimer.stop(),
				setattr(self, '_batteryBlink', False),
			),
			self.updateBatteryIcon(),
		) )
		def uiBatteryIconPaintEvent(evt, rectSize=24):
			"""Draw the little coloured square on the focus peaking button."""
			if self._batteryPresent and (self._batteryCharging or not self._batteryBlink):
				powerDownLevel = api.apiValues.get('powerOffWhenMainsLost') * self.uiPowerDownThreshold
				warningLevel = powerDownLevel + 0.15
				
				x,y,w,h = (
					1,
					1,
					self.uiBatteryIcon.width() - 2,
					self.uiBatteryIcon.height() - 1,
				)
				
				p = QPainter(self.uiBatteryIcon)
				
				#Cut out the battery outline, so the battery fill level doesn't show by
				#outside the "nub". Nextly, this was taken care of by an opaque box
				#outside the battery nub in the SVG image, but this didn't work so well
				#when the button was pressed or when themes were changed. We can't fill
				#a polygon a percentage of the way very easily, and we can't just go in
				#and muck with the SVG to achieve this either like we would in browser.
				batteryOutline = QPainterPath()
				batteryOutline.addPolygon(QPolygonF([
					QPoint(x+3,y),
					QPoint(x+3,y+2), #Left battery nub chunk.
					QPoint(x,y+2),
					QPoint(x,y+h), #Bottom
					QPoint(x+w,y+h),
					QPoint(x+w,y+2),
					QPoint(x+w-3,y+2), #Right battery nub chunk.
					QPoint(x+w-3,y),
				]))
				batteryOutline.closeSubpath() #Top of battery nub.
				p.setClipPath(batteryOutline, Qt.IntersectClip)
				
				p.setPen(QPen(QColor('transparent')))
				
				if self._batteryCharge > warningLevel or self._batteryCharging:
					p.setBrush(QBrush(QColor('#00b800')))
				else:
					p.setBrush(QBrush(QColor('#f20000')))
				p.drawRect(
					x, y + h * (1-self._batteryCharge),
					w, h * self._batteryCharge )
			type(self.uiBatteryIcon).paintEvent(self.uiBatteryIcon, evt) #Invoke the superclass to paint the battery overlay image on our new rect.
		self.uiBatteryIcon.paintEvent = uiBatteryIconPaintEvent
		
		
		#Record / stop
		self.uiRecordTemplateWithTime = self.uiRecord.text()
		self.uiRecordTemplateNoTime = self.uiRecordTemplateWithTime.split('\n')[0][2:]
		self.uiRecord.clicked.connect(self.toggleRecording)
		
		def uiRecordPaintEventRecord(evt, iconSize=24, offsetX=32):
			midpoint = self.uiRecord.geometry().size()/2 - QSize(0, self.uiRecord.touchMargins()['bottom']/2)
			p = QPainter(self.uiRecord)
			p.setPen(QPen(QColor('#000000')))
			p.setBrush(QBrush(QColor('#f20000')))
			p.setRenderHint(QPainter.Antialiasing, True)
			p.drawChord( #xy/wh
				midpoint.width()-iconSize/2-offsetX, midpoint.height()-iconSize/2,
				iconSize, iconSize,
				0, 16*360, #start, end angle
			)
		def uiRecordPaintEventPause(evt, iconSize=20, offsetX=24):
			midpoint = self.uiRecord.geometry().size()/2 - QSize(0, self.uiRecord.touchMargins()['bottom']/2)
			p = QPainter(self.uiRecord)
			p.setPen(QPen(QColor('#ffffff')))
			p.setBrush(QBrush(QColor('#000000')))
			p.drawRect( #xy/wh
				midpoint.width()-iconSize/2-offsetX, midpoint.height()-iconSize/2,
				iconSize/3, iconSize,
			)
			p.drawRect( #xy/wh
				midpoint.width()-iconSize/2-offsetX+iconSize/3*2, midpoint.height()-iconSize/2,
				iconSize/3, iconSize,
			)
		def uiRecordPaintEventStop(evt, iconSize=20, offsetX=24):
			midpoint = self.uiRecord.geometry().size()/2 - QSize(0, self.uiRecord.touchMargins()['bottom']/2)
			p = QPainter(self.uiRecord)
			p.setPen(QPen(QColor('#ffffff')))
			p.setBrush(QBrush(QColor('#000000')))
			p.drawRect( #xy/wh
				midpoint.width()-iconSize/2-offsetX, midpoint.height()-iconSize/2,
				iconSize, iconSize,
			)
			self.uiRecord.setText( #Do the timer.
				self.uiRecordTemplateWithTime.format(
					state="Stop",
					timeRecorded=(recordingEndTime or time()) - recordingStartTime,
				)
			)
		def uiRecordPaintEvent(evt):
			type(self.uiRecord).paintEvent(self.uiRecord, evt)
			#TODO DDR 2019-10-07: Add pause icon, when we are able to pause recording and resume again, for run 'n' gun mode.
			if self.uiRecord.isRecording:
				uiRecordPaintEventRecord(evt)
			else:
				uiRecordPaintEventStop(evt)
		self.uiRecord.paintEvent = uiRecordPaintEvent
		
		api.observe('state', self.onStateChange)
		
		#Play & save
		#TODO DDR 2019-09-27 fill in play and save
		uiPlayAndSaveTemplate = self.uiPlayAndSave.text()
		self.uiPlayAndSave.setText("Play && Save\n-1s RAM\n-1s Avail.")
		
		playAndSaveData = self.control.getSync(['cameraMaxFrames', 'frameRate', 'recSegments'])
		def updatePlayAndSaveText(*_):
			data = playAndSaveData
			segmentMaxRecTime = data['cameraMaxFrames'] / data['frameRate'] / data['recSegments']
			segmentCurrentRecTime = min(
				segmentMaxRecTime, 
				(recordingEndTime or time()) - recordingStartTime
			)
			self.uiPlayAndSave.setText(
				uiPlayAndSaveTemplate.format(
					ramUsed=segmentCurrentRecTime, ramTotal=segmentMaxRecTime ) )
		updatePlayAndSaveText()
			
		def updatePlayAndSaveDataMaxFrames(value):
			playAndSaveData['cameraMaxFrames'] = value
		api.observe_future_only('cameraMaxFrames', updatePlayAndSaveDataMaxFrames)
		api.observe_future_only('cameraMaxFrames', updatePlayAndSaveText)
		
		def updatePlayAndSaveDataFrameRate(_):
			playAndSaveData['frameRate'] = self.control.getSync('frameRate')
		api.observe_future_only('framePeriod', updatePlayAndSaveDataFrameRate)
		api.observe_future_only('framePeriod', updatePlayAndSaveText)
		
		def updatePlayAndSaveDataRecSegments(value):
			playAndSaveData['recSegments'] = value
		api.observe_future_only('recSegments', updatePlayAndSaveDataRecSegments)
		api.observe_future_only('recSegments', updatePlayAndSaveText)
		
		def uiPlayAndSaveDraw(evt):
			type(self.uiPlayAndSave).paintEvent(self.uiPlayAndSave, evt)
			updatePlayAndSaveText() #Gotta schedule updates like this, because using a timer clogs the event pipeline full of repaints and updates wind up being extremely slow.
		self.uiPlayAndSave.paintEvent = uiPlayAndSaveDraw
		
		self.uiPlayAndSave.clicked.connect(lambda:
			window.show('play_and_save')) #This should prompt to record if no footage is recorded, and explain itself.
		
		
		#Storage media
		uiExternalMediaTemplate = self.uiExternalMedia.text()
		externalMediaUUID = ''
		externalMediaRatioFull = -1 #Don't draw the bar if negative.
		
		def updateExternalMediaPercentFull(percent):
			nonlocal externalMediaRatioFull
			if externalMediaRatioFull != percent:
				externalMediaRatioFull = percent
				self.uiExternalMedia.update()
		
		
		self.uiExternalMedia.setText("No Save\nMedia Found") #Without this, there is an unavoidable FOUC unless we call df sychronously. So we lie and just don't detect it at first. 😬
		def updateExternalMediaText():
			"""Update the external media text. This will called every few seconds to update the %-free display. Also repaints %-bar."""
			partitions = ([
				partition
				for partition in api.externalPartitions.list()
				if partition['uuid'] == externalMediaUUID
			] or api.externalPartitions.list())[:1]
			if not partitions:
				updateExternalMediaPercentFull(-1)
				self.uiExternalMedia.setText(
					"No Save\nMedia Found" )
			else:
				partition = partitions[0]
				def updateExternalMediaTextCallback(space):
					saved = estimateFile.duration(space['used'] * 1000)
					total = estimateFile.duration(space['total'] * 1000)
					updateExternalMediaPercentFull(space['used']/space['total']),
					
					self.uiExternalMedia.setText(
						uiExternalMediaTemplate.format(
							externalStorageIdentifier = partition['name'] or f"{round(partition['size'] / 1e9):1.0f}GB Storage Media",
							percentFull = round(space['used']/space['total'] * 100),
							footageSavedDuration = '-1', #TODO: Calculate bits per second recorded and apply it here to the partition usage and total.
							hoursSaved = saved.days*24 + saved.seconds/60/60,
							minutesSaved = (saved.seconds/60) % 60,
							secondsSaved = saved.seconds % 60,
							hoursTotal = total.days*24 + total.seconds/60/60,
							minutesTotal = (total.seconds/60) % 60,
							secondsTotal = total.seconds % 60,
						)
					)
				api.externalPartitions.usageFor(partition['device'], 
					updateExternalMediaTextCallback )
		self.updateExternalMediaText = updateExternalMediaText #oops, assign this to self so we can pre-call the timer on show.
		
		def updateExternalMediaUUID(uuid):
			self.externalMediaUUID = uuid
			updateExternalMediaText()
		settings.observe('preferredFileSavingUUID', '', updateExternalMediaUUID)
		
		api.externalPartitions.observe(lambda partitions:
			updateExternalMediaText() )
		
		self._externalMediaUsagePollTimer = QtCore.QTimer()
		self._externalMediaUsagePollTimer.timeout.connect(updateExternalMediaText)
		self._externalMediaUsagePollTimer.setTimerType(QtCore.Qt.VeryCoarseTimer) #Infrequent, wake as little as possible.
		self._externalMediaUsagePollTimer.setInterval(15000) #This should almost never be needed, it just covers if another program is writing or deleting something from the disk.
		
		def uiExternalMediaPaintEvent(evt, meterPaddingX=15, meterOffsetY=2, meterHeight=10):
			"""Draw the disk usage bar on the external media button."""
			type(self.uiExternalMedia).paintEvent(self.uiExternalMedia, evt) #Invoke the superclass to - hopefully - paint the rest of the button before we deface it with our square.
			if externalMediaRatioFull == -1:
				return
			
			midpoint = self.uiExternalMedia.geometry().size()/2 + QSize(0, self.uiExternalMedia.touchMargins()['top']/2)
			type(self.uiExternalMedia).paintEvent(self.uiExternalMedia, evt) #Invoke the superclass to - hopefully - paint the rest of the button before we deface it with our square.
			p = QPainter(self.uiExternalMedia)
			p.fillRect( #xywh
				meterPaddingX,
				midpoint.height() + meterOffsetY,
				#TODO DDR 2019-09-27: When we know how long the current recorded clip is, in terms of external media capacity, add it as a white rectangle here.
				0, #(midpoint.width() - meterPaddingX) * 2 * externalMediaRatioFull + ???,
				meterHeight,
				QColor('white')
			)
			p.fillRect( #xywh
				meterPaddingX,
				midpoint.height() + meterOffsetY,
				(midpoint.width() - meterPaddingX) * 2 * externalMediaRatioFull,
				meterHeight,
				QColor('#00b800')
			)
			p.setPen(QPen(QColor('black')))
			p.setBrush(QBrush(QColor('transparent')))
			p.drawRect( #xywh
				meterPaddingX,
				midpoint.height() + meterOffsetY,
				(midpoint.width() - meterPaddingX) * 2,
				meterHeight
			)
		self.uiExternalMedia.paintEvent = uiExternalMediaPaintEvent
		
		self.uiExternalMedia.clicked.connect(lambda:
			window.show('file_settings') )
 def isTime(string):
     return regex_match(
         r'(\d)?\d:\d\d(pm|am|PM|AM)?', string) or regex_match(
             r'\d\d?(pm|am|PM|AM)', string)
Exemplo n.º 25
0
def get_day_folder_path(prefix, year, month, day):
    """Constructs and returns the day path string"""
    assert regex_match(r'^(\d\d)?\d\d$', year), "%s is not a valid year" % year
    assert regex_match(r'^(\d)?\d$', month), "%s is not a valid month" % month
    assert regex_match(r'^(\d)?\d$', day), "%s is not a valid day" % day
    return "%s/year=%d/month=%d/day=%d/" % (prefix, int(year), int(month), int(day))
Exemplo n.º 26
0
    def _execute(self):
        self._process(
            cmd=
            f"apt install -y {' '.join([pkg for pkg in self.UPDATE_PACKAGES])}",
            msg='Installing update dependencies',
        )

        # building config
        apache_tmp_folder = [
            _dir for _dir in listdir('/tmp') if regex_match(
                'systemd-private-.*-apache2.service.*', _dir) is not None
        ][0]
        config_file = f"/tmp/{apache_tmp_folder}/tmp/ga_update.conf"

        with open(config_file, 'r') as file:
            for line in file.readlines():
                _key, _value = line.split('=')
                key = _key.strip()
                value = _value.replace('\n', '').strip()
                self.config[key] = value

        results = []

        # preparing update script
        if self.config['ga_update_path_repo'] in self.NONE_RESULTS:
            if self.config['ga_update_method'] != 'offline':
                results.append(
                    self._process(
                        cmd=
                        f"git clone https://github.com/superstes/growautomation.git --single-branch {self.repo}",
                        msg='Downloading git repository',
                    ))
                self.config['ga_update_path_repo'] = self.repo

            else:
                journal.send(
                    "ERROR: No valid repository provided and update-mode is offline. Can't continue!"
                )
                return False

        else:
            self.repo = self.config['ga_update_path_repo']

        # NOTE: we do this since it could lead to incompatibility problems in the future if we use the newest version of the update-script
        target_version = self.config['ga_update_release'] if self.config[
            'ga_update_type'] != 'commit' else self.config['ga_update_commit']
        results.append(
            self._process(
                cmd=f"cd {self.repo} && git reset --hard {target_version}",
                msg='Getting correct script-version',
            ))

        vars_string = ' '.join([
            f"--extra-vars '{key}={value}'"
            for key, value in self.config.items()
        ])
        path_ansible = f"{self.repo}/setup"

        results.append(
            self._process(
                cmd=
                f"cd {path_ansible} && ansible-galaxy collection install -r requirements.yml",
                msg="Installing update-script requirements (ansible)"))

        results.append(
            self._process(
                cmd=
                f"cd {path_ansible} && ansible-playbook pb_update.yml {vars_string}",
                msg="Starting ansible-playbook to update GrowAutomation!"))

        return all(results)
Exemplo n.º 27
0
def do_macro(parser, token):
    """ the function taking the parsed tag and returning
    a DefineMacroNode object.
    """
    try:
        bits = token.split_contents()
        tag_name, macro_name, arguments = bits[0], bits[1], bits[2:]
    except IndexError:
        raise template.TemplateSyntaxError(
            "'{0}' tag requires at least one argument (macro name)".format(
                token.contents.split()[0]))

    # use regex's to parse the arguments into arg
    # and kwarg definitions

    # the regex for identifying python variable names is:
    #  r'^[A-Za-z_][\w_]*$'

    # args must be proper python variable names
    # we'll want to capture it from the regex also.
    arg_regex = r'^([A-Za-z_][\w_]*)$'

    # kwargs must be proper variable names with a
    # default value, name="value", or name=value if
    # value is a template variable (potentially with
    # filters).
    # we'll want to capture the name and value from
    # the regex as well.
    kwarg_regex = (
        r'^([A-Za-z_][\w_]*)=(".*"|{0}.*{0}|[A-Za-z_][\w_]*)$'.format("'"))
    # leave further validation to the template variable class

    args = []
    kwargs = {}
    for argument in arguments:
        arg_match = regex_match(
            arg_regex, argument)
        if arg_match:
            args.append(arg_match.groups()[0])
        else:
            kwarg_match = regex_match(
                kwarg_regex, argument)
            if kwarg_match:
                kwargs[kwarg_match.groups()[0]] = template.Variable(
                    # convert to a template variable here
                    kwarg_match.groups()[1])
            else:
                raise template.TemplateSyntaxError(
                    "Malformed arguments to the {0} tag.".format(
                        tag_name))

    # parse to the endmacro tag and get the contents
    nodelist = parser.parse(('endmacro',))
    parser.delete_first_token()

    # store macro in parser._macros, creating attribute
    # if necessary
    _setup_macros_dict(parser)
    parser._macros[macro_name] = DefineMacroNode(
        macro_name, nodelist, args, kwargs)
    return parser._macros[macro_name]
Exemplo n.º 28
0
async def post_handler_match(request):
    """
    Create a new Match
    :param request:
        home_team : String - the home team name
        away_team : String - the away team name
        score : String - the match score
        date : String - the match date in format: YYYY-MM-DD
    :return
    request example
        {
        }
    """
    logger.info(f'Server/Create Ended Match - start | request: {request.json}')
    try:
        logger.debug(
            f'Server/Create Ended Match - calling validate_schema | request: {request.json}, schema: {MatchSchema}'
        )
        validate_schema(instance=request.json, schema=MatchSchema)
        date = request.json.get("date", None)
        if date != datetime.strptime(date, "%Y-%m-%d").strftime('%Y-%m-%d'):
            raise ValueError('Date must be in the format: YYYY-MM-DD')
        score = request.json.get("score", None).replace(' ', '')
        is_valid_score = regex_match("(0|[1-9]\d*)-(0|[1-9]\d*)", score)
        if not is_valid_score:
            raise ValueError('Score must be in the format: Number-Number')
        logger.debug('Server/Create Ended Match - input validation succeeded')

        logger.debug(
            f'Server/Create Ended Match - calling matchProvider/parse_match_from_request | request: {request.json}, schema: {MatchSchema}'
        )
        parsed_request = parse_ended_match_from_request(request.json)
        logger.debug(
            f'Server/Create Ended Match - matchProvider/parse_match_from_request succeeded | parsed request: {parsed_request}'
        )

        logger.debug(
            f'Server/Create Ended Match - calling MongoDbService/create_match_with_score | request: {parsed_request}'
        )
        _id = db.create_match_with_score(parsed_request)
        logger.debug(
            f'Server/Create Ended Match - MongoDbService/create_match_with_score succeeded | match id : {_id}'
        )

    except (ValidationError, ValueError) as error:
        logger.error(
            f'Server/Create Ended Match failed - validation error | error: {error}'
        )
        return rjson({'status': 'Error', 'message': str(error)}, status=400)

    except Exception as error:
        logger.error(f'Server/Create Ended Match failed - error: {error}')
        return rjson({'status': 'Error', 'message': str(error)}, status=400)

    else:
        logger.info(f'Server/Create Ended Match succeeded - id: {_id}')
        return rjson(
            {
                'status': "success",
                'message': 'the match added',
                'id': str(_id)
            },
            status=200)

    finally:
        logger.info(f'Server/Create Ended Match - end')