Initial commit

This commit is contained in:
Disassembler 2023-09-09 10:41:07 +02:00
commit 705c85177a
Signed by: Disassembler
GPG Key ID: 524BD33A0EE29499
27 changed files with 1487 additions and 0 deletions

0
api/__init__.py Normal file
View File

244
api/app.py Normal file
View File

@ -0,0 +1,244 @@
from datetime import datetime, timedelta
from flask import Flask, request
from .data import DataFile, DateTimeSpan, MinuteSpan
DATA = DataFile("data.json")
app = Flask(__name__)
def get_action_at(point_in_time, schedules, overrides):
for override in overrides:
if override.start <= point_in_time < override.end:
return override.action
for span in schedules[point_in_time.weekday()]:
span = DateTimeSpan.from_minutespan(point_in_time, span)
if span.start <= point_in_time < span.end:
return True
return False
def get_next_action_change(point_in_time, action, schedules, overrides):
skip_schedule = False
if not any(schedules.values()):
# No daily schedule is set
overrides = [o for o in overrides if o.action]
if not overrides:
# No allow overrides exist => denied indefinitely
return None
skip_schedule = True
elif all(o == [MinuteSpan(0, 24 * 60)] for o in schedules.values()):
# Daily schedule allowed 24/7
overrides = [o for o in overrides if not o.action]
if not overrides:
# No deny overrides exist => allowed indefinitely
return None
skip_schedule = True
if skip_schedule:
# Schedule is either empty or allows 24/7, therefore irrelevant
# and only overrides are considered.
for override_span in overrides:
if override_span.start > point_in_time:
# Nearest override didn't start yet
return override_span.start
if override_span.start <= point_in_time < override_span.end:
# We're in override
return override_span.end
# No more overrides => allowed / denied indefinitely
return None
while True:
new_point_in_time = None
for override_span in overrides:
if override_span.start <= point_in_time < override_span.end:
# We're in override
new_point_in_time = override_span.end
if new_point_in_time is None:
dow = point_in_time.weekday()
for span in schedules[dow]:
span = DateTimeSpan.from_minutespan(point_in_time, span)
if span.end < point_in_time:
# Skip already elapsed spans
continue
if span.start <= point_in_time < span.end:
# We're in span allowed by schedule, get the span end
new_point_in_time = span.end
# Check if any upcoming override comes sooner that the span end
for override_span in overrides:
if override_span.start >= point_in_time and override_span.start < span.end:
new_point_in_time = override_span.start
break
break
if new_point_in_time is None:
# We're in span denied by schedule, get next span start
days = 0
while True:
for span in schedules[(dow + days) % 7]:
span = DateTimeSpan.from_minutespan(point_in_time + timedelta(days=days), span)
if span.start > point_in_time:
new_point_in_time = span.start
# Check if any upcoming override comes sooner that the span start
for override_span in overrides:
if (
override_span.start >= point_in_time
and override_span.start < span.start
):
new_point_in_time = override_span.start
break
break
if new_point_in_time:
break
days += 1
if action != get_action_at(new_point_in_time, schedules, overrides):
return new_point_in_time
point_in_time = new_point_in_time
def update_status(status, schedules, overrides):
now = datetime.now()
status.action = get_action_at(now, schedules, overrides)
status.until = get_next_action_change(now, status.action, schedules, overrides)
status.then_until = (
get_next_action_change(status.until, not status.action, schedules, overrides)
if status.until
else None
)
def update_schedule(schedule, span, allow):
if allow:
schedule.append(span)
schedule.sort()
i = len(schedule) - 1
while i:
if schedule[i - 1].end >= schedule[i].start:
# spans overlap -> merge together
schedule[i - 1].start = min(schedule[i - 1].start, schedule[i].start)
schedule[i - 1].end = max(schedule[i - 1].end, schedule[i].end)
del schedule[i]
i -= 1
else:
i = len(schedule) - 1
while i >= 0:
if span.start <= schedule[i].start and span.end >= schedule[i].end:
# deny period larger than span -> remove span
del schedule[i]
elif span.start > schedule[i].start and span.end < schedule[i].end:
# deny period shorter than span -> split span into two
schedule.append(MinuteSpan(span.end, schedule[i].end))
schedule[i].end = span.start
elif span.start <= schedule[i].start < span.end:
# span starts within deny period -> move start
schedule[i].start = span.end
elif span.start < schedule[i].end <= span.end:
# span ends within deny period -> move end
schedule[i].end = span.start
i -= 1
schedule.sort()
def update_overrides(overrides, span):
now = datetime.now()
if span.end <= now:
# The span is in the past, nothing to do
return
i = len(overrides) - 1
while i >= 0:
if span.start <= overrides[i].start and span.end >= overrides[i].end:
# new override period larger than old override -> remove old override
del overrides[i]
elif span.start > overrides[i].start and span.end < overrides[i].end:
if span.start > now:
# new override period shorter than old override -> split old override into two
overrides.append(DateTimeSpan(span.end, overrides[i].end, overrides[i].action))
overrides[i].end = span.start
else:
# new override period would split the override, but the first part
# has already elapsed -> move old override start
overrides[i].start = span.end
elif span.start <= overrides[i].start < span.end:
# new override starts within old overrides period -> move old override start
overrides[i].start = span.end
elif span.start < overrides[i].end <= span.end:
# new override ends within old overrides period -> move old override end
overrides[i].end = span.start
i -= 1
overrides.append(span)
overrides.sort()
i = len(overrides) - 1
while i:
if (
overrides[i - 1].end >= overrides[i].start
and overrides[i - 1].action == overrides[i].action
):
# overrides of the same type overlap -> merge together
overrides[i - 1].start = min(overrides[i - 1].start, overrides[i].start)
overrides[i - 1].end = max(overrides[i - 1].end, overrides[i].end)
del overrides[i]
i -= 1
@app.get("/data")
def get_data():
with DATA as data:
if data.status.until and datetime.now() >= data.status.until:
update_status(data.status, data.schedules, data.overrides)
data.save()
return data.to_dict()
@app.post("/override")
def post_override():
span = DateTimeSpan.from_args(**request.json)
with DATA as data:
update_overrides(data.overrides, span)
update_status(data.status, data.schedules, data.overrides)
data.save()
return data.to_dict()
@app.post("/delete-override")
def delete_override():
span = DateTimeSpan.from_args(**request.json)
with DATA as data:
try:
data.overrides.remove(span)
update_status(data.status, data.schedules, data.overrides)
data.save()
except ValueError:
pass
return data.to_dict()
@app.post("/schedule")
def post_schedule():
schedule = request.json
span = MinuteSpan.from_args(**schedule)
with DATA as data:
for day in schedule["days"]:
update_schedule(data.schedules[day], span, schedule["action"])
update_status(data.status, data.schedules, data.overrides)
data.save()
return data.to_dict()
@app.get("/fast-action/<int:minutes>")
def fast_action(minutes):
with DATA as data:
now = datetime.now().replace(second=0, microsecond=0)
if minutes:
start = now
action = get_action_at(now, data.schedules, data.overrides)
if action:
start = get_next_action_change(now, action, data.schedules, data.overrides)
end = start + timedelta(minutes=minutes)
span = DateTimeSpan(start, end, True)
else:
span = DateTimeSpan(now, now + timedelta(minutes=5), False)
update_overrides(data.overrides, span)
update_status(data.status, data.schedules, data.overrides)
data.save()
return data.to_dict()

144
api/data.py Normal file
View File

@ -0,0 +1,144 @@
import fcntl
import json
from datetime import datetime, timedelta
from pathlib import Path
MINS = "minutes"
class Span:
def __init__(self, start, end):
self.start = min(start, end)
self.end = max(start, end)
def __eq__(self, other):
return (self.start, self.end) == (other.start, other.end)
def __lt__(self, other):
return (self.start, self.end) < (other.start, other.end)
class MinuteSpan(Span):
@classmethod
def from_args(cls, **kwargs):
start = kwargs["start"].split(":")
start = int(start[0]) * 60 + int(start[1])
end = kwargs["end"].split(":")
end = int(end[0]) * 60 + int(end[1])
if end == 0:
# To 00:00 should mean until midnight (24:00)
end = 24 * 60
return cls(start, end)
def to_dict(self):
return {
"start": f"{self.start // 60}:{self.start % 60:02}",
"end": f"{self.end // 60}:{self.end % 60:02}",
}
class DateTimeSpan(Span):
def __init__(self, start, end, action):
super().__init__(start, end)
self.action = action
def __eq__(self, other):
return (self.start, self.end, self.action) == (other.start, other.end, other.action)
@classmethod
def from_args(cls, **kwargs):
return cls(
datetime.fromisoformat(kwargs["start"]),
datetime.fromisoformat(kwargs["end"]),
kwargs["action"],
)
@classmethod
def from_minutespan(cls, date, minutespan):
start = minutespan.start
end = minutespan.end
add_days = 0
if end == 24 * 60:
# Ending midnight needs to be set as 0:00 of the next day
end = 0
add_days = 1
return cls(
date.replace(hour=start // 60, minute=start % 60, second=0, microsecond=0),
date.replace(hour=end // 60, minute=end % 60, second=0, microsecond=0)
+ timedelta(days=add_days),
True,
)
def to_dict(self):
return {
"start": self.start.isoformat(timespec=MINS),
"end": self.end.isoformat(timespec=MINS),
"action": self.action,
}
class Status:
def __init__(self, action, until=None, then_until=None):
self.action = action
self.until = until
self.then_until = then_until
@classmethod
def from_args(cls, **kwargs):
until = kwargs.get("until")
then_until = kwargs.get("then_until")
return cls(
kwargs["action"],
datetime.fromisoformat(until) if until else None,
datetime.fromisoformat(then_until) if then_until else None,
)
def to_dict(self):
return {
"action": self.action,
"until": self.until.isoformat(timespec=MINS) if self.until else None,
"then_until": self.then_until.isoformat(timespec=MINS) if self.then_until else None,
}
class DataFile:
def __init__(self, path):
self._path = Path(path)
self._file = None
self.status = None
self.schedules = None
self.overrides = None
def __enter__(self):
self._file = self._path.open("r+", encoding="utf-8")
fcntl.flock(self._file, fcntl.LOCK_EX)
data = json.load(self._file)
self.status = Status.from_args(**data.get("status", {"action": False}))
self.schedules = {
int(day): [MinuteSpan.from_args(**span) for span in schedule]
for day, schedule in data.get("schedules", {day: [] for day in range(7)}).items()
}
overrides = [DateTimeSpan.from_args(**override) for override in data.get("overrides", [])]
# Remove expired overrides
now = datetime.now()
self.overrides = [o for o in overrides if o.end > now]
return self
def __exit__(self, exc_type, exc_value, traceback):
self._file.close()
def to_dict(self):
return {
"status": self.status.to_dict(),
"schedules": {
day: [span.to_dict() for span in schedule]
for day, schedule in self.schedules.items()
},
"overrides": [override.to_dict() for override in self.overrides],
}
def save(self):
self._file.seek(0)
self._file.truncate(0)
json.dump(self.to_dict(), self._file)

View File

@ -0,0 +1,5 @@
<Project Sdk="WixToolset.Sdk/4.0.0">
<ItemGroup>
<ProjectReference Include="..\ParentalControlService\ParentalControlService.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,41 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package Name="Marek Parental Control" Manufacturer="Disassembler" Version="1.0.0.0" UpgradeCode="feb429a6-684e-4506-816f-006384136754" Compressed="true">
<!-- Allow upgrades and prevent downgrades -->
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
<!-- Define the directory structure -->
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLDIR" Name="Marek Parental Control" />
</StandardDirectory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="ServiceExecutable" Bitness="always64">
<File Id="ParentalControlService.exe"
Source="$(var.ParentalControlService.TargetDir)publish\ParentalControlService.exe"
KeyPath="true" />
<RemoveFile Id="ALLFILES" Name="*.*" On="both" />
<ServiceInstall Id="ServiceInstaller"
Type="ownProcess"
Name="ParentalControlService"
DisplayName="Marek Parental Control"
Description="Marek Parental Control"
Start="auto"
ErrorControl="normal" />
<ServiceControl Id="StartService"
Start="install"
Stop="both"
Remove="uninstall"
Name="ParentalControlService"
Wait="true" />
</Component>
</DirectoryRef>
<Feature Id="Service" Title="Marek Parental Control Setup" Level="1">
<ComponentRef Id="ServiceExecutable" />
</Feature>
</Package>
</Wix>

View File

@ -0,0 +1,61 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34024.191
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParentalControlService", "ParentalControlService\ParentalControlService.csproj", "{050D342F-0A53-4A08-B56A-AA63A22A74A9}"
EndProject
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Installer", "Installer\Installer.wixproj", "{C4838DE1-3AC8-4931-80C2-340EBA94996F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|ARM64.Build.0 = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x64.ActiveCfg = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x64.Build.0 = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x86.ActiveCfg = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x86.Build.0 = Debug|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|Any CPU.Build.0 = Release|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|ARM64.ActiveCfg = Release|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|ARM64.Build.0 = Release|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x64.ActiveCfg = Release|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x64.Build.0 = Release|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x86.ActiveCfg = Release|Any CPU
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x86.Build.0 = Release|Any CPU
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|Any CPU.ActiveCfg = Debug|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|Any CPU.Build.0 = Debug|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|ARM64.ActiveCfg = Debug|ARM64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|ARM64.Build.0 = Debug|ARM64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x64.ActiveCfg = Debug|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x64.Build.0 = Debug|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x86.ActiveCfg = Debug|x86
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x86.Build.0 = Debug|x86
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|Any CPU.ActiveCfg = Release|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|Any CPU.Build.0 = Release|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|ARM64.ActiveCfg = Release|ARM64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|ARM64.Build.0 = Release|ARM64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x64.ActiveCfg = Release|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x64.Build.0 = Release|x64
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x86.ActiveCfg = Release|x86
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E4EF7269-DAC7-4C33-9AF1-FCAEBAE0AD77}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace ParentalControlService {
public struct Status
{
[JsonPropertyName("action")]
public bool Action { get; set; }
[JsonPropertyName("until")]
public DateTime? Until { get; set; }
[JsonPropertyName("then_until")]
public DateTime? ThenUntil { get; set; }
public override bool Equals(object? obj) {
return obj is Status other && Equals(other);
}
public readonly bool Equals(Status other) {
return Action == other.Action && Until == other.Until && ThenUntil == other.ThenUntil;
}
public override readonly int GetHashCode() {
return HashCode.Combine(Action, Until, ThenUntil);
}
public static bool operator ==(Status lhs, Status rhs) {
return lhs.Action == rhs.Action && lhs.Until == rhs.Until && lhs.ThenUntil == rhs.ThenUntil;
}
public static bool operator !=(Status lhs, Status rhs) {
return !(lhs == rhs);
}
}
public struct Schedule
{
[JsonPropertyName("start")]
public string? Start { get; set; }
[JsonPropertyName("end")]
public string? End { get; set; }
}
public struct Override
{
[JsonPropertyName("start")]
public DateTime Start { get; set; }
[JsonPropertyName("end")]
public DateTime End { get; set; }
[JsonPropertyName("action")]
public bool Action { get; set; }
}
public struct Data
{
[JsonPropertyName("status")]
public Status Status { get; set; }
[JsonPropertyName("schedules")]
public Dictionary<int,List<Schedule>> Schedules { get; set; }
[JsonPropertyName("overrides")]
public List<Override> Overrides { get; set; }
}
public struct Config
{
[JsonPropertyName("url")]
public string Url { get; set; }
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("password")]
public string Password { get; set; }
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<UserSecretsId>dotnet-ParentalControlService-c499de7d-03ae-4d69-a7cf-e634d0dce29d</UserSecretsId>
<OutputType>exe</OutputType>
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<Version>1.1.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>C:\Users\Disassembler\Desktop\client\ParentalControlService\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,134 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Timer = System.Timers.Timer;
namespace ParentalControlService;
public class ParentalControlSvc : BackgroundService
{
private readonly string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
private readonly string statusPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "status.json");
private readonly ILogger<ParentalControlSvc> _logger;
private Config config;
private Status status;
private bool currentAction;
private readonly Timer checkTimer;
private readonly HttpClient httpClient;
public ParentalControlSvc(ILogger<ParentalControlSvc> logger)
{
_logger = logger;
// Load status
try {
status = JsonSerializer.Deserialize<Status>(File.ReadAllText(statusPath))!;
}
catch (FileNotFoundException) {
status = new Status() { Action = true, Until = DateTime.MaxValue };
}
currentAction = status.Action;
// Load config
try
{
config = JsonSerializer.Deserialize<Config>(File.ReadAllText(configPath))!;
}
catch (FileNotFoundException)
{
config = new Config() { Username = "", Password = "" };
}
// Create HTTP Client
httpClient = new() { BaseAddress = new Uri(config.Url), Timeout = TimeSpan.FromSeconds(5) };
string credentials = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{config.Username}:{config.Password}"));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("ParentalControl", "1.1.0"));
// Initialize Timer
checkTimer = new(5000);
checkTimer.Elapsed += new ElapsedEventHandler(CheckTimerTick);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try {
await CheckTimerTick();
checkTimer.Start();
while (!stoppingToken.IsCancellationRequested) {
await Task.Delay(1000, stoppingToken);
}
}
catch (TaskCanceledException) {
checkTimer.Stop();
}
catch (Exception ex) {
_logger.LogError(ex, "{Message}", ex.Message);
Environment.Exit(1);
}
}
private void SaveStatus() {
File.WriteAllText(statusPath, JsonSerializer.Serialize(status));
}
private static void RunScript(string scriptName) {
scriptName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{scriptName}.ps1");
if (!File.Exists(scriptName)) {
return;
}
ProcessStartInfo psi = new() {
FileName = "powershell.exe",
Arguments = $"-ExecutionPolicy Bypass -File \"{scriptName}\"",
UseShellExecute = false,
WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory,
};
Process.Start(psi);
}
private async Task CheckTimerTick() {
try {
Data data = await httpClient.GetFromJsonAsync<Data>("api/data");
if (data.Status != status) {
status = data.Status;
SaveStatus();
}
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) {
_logger.LogError(ex, "{Message}", ex.Message);
}
if (status.Until != null && DateTime.Now >= status.Until) {
// Fetch failed, we need to advance the schedule on our own
status.Action = !status.Action;
status.Until = status.ThenUntil;
status.ThenUntil = null;
SaveStatus();
}
if (currentAction == status.Action) {
return;
}
currentAction = !currentAction;
if (currentAction) {
_logger.LogInformation("[{time}] Unblocked", DateTime.Now);
RunScript("unblock");
}
else {
_logger.LogInformation("[{time}] Blocked", DateTime.Now);
RunScript("block");
}
}
private async void CheckTimerTick(object? sender, ElapsedEventArgs e) {
await CheckTimerTick();
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;
using Microsoft.Extensions.Logging;
using ParentalControlService;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options => {
options.ServiceName = "Marek Parental Control";
});
LoggerProviderOptions.RegisterProviderOptions<EventLogSettings, EventLogLoggerProvider>(builder.Services);
builder.Services.AddSingleton<ParentalControlSvc>();
builder.Services.AddHostedService<ParentalControlSvc>();
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
IHost host = builder.Build();
host.Run();

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net7.0-windows\win-x64\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net7.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,11 @@
{
"profiles": {
"ParentalControlService": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

8
client/block.ps1 Normal file
View File

@ -0,0 +1,8 @@
Start-Sleep 1
C:\Windows\System32\net.exe user Marek /active:no
$session_id = ((C:\Windows\System32\quser.exe | Where-Object {$_ -match 'Marek' }) -split ' +')[2]
If ($session_id) {
logoff $session_id
Start-Sleep 1
shutdown /s /f /t 0
}

5
client/config.json Normal file
View File

@ -0,0 +1,5 @@
{
"url": "https://pc.dasm.cz",
"username": "parent",
"password": "DA2ptE{!my_hSN6="
}

1
client/unblock.ps1 Normal file
View File

@ -0,0 +1 @@
C:\Windows\System32\net.exe user Marek /active:yes

1
data.json Normal file
View File

@ -0,0 +1 @@
{"schedules": {"0": [], "1": [], "2": [], "3": [], "4": [], "5": [], "6": []}, "overrides": []}

71
index.html Normal file
View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<title>Parental Control</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="stylesheet" href="static/style.css" type="text/css" media="all">
<script type="text/javascript" src="static/script.js"></script>
</head>
<body>
<div>
<h2>Stav</h2>
<div id="status">&nbsp;</div>
<input class="fast-action" data-mins="15" type="button" value="+15 min">
<input class="fast-action" data-mins="30" type="button" value="+30 min">
<input class="fast-action" data-mins="60" type="button" value="+1 hod">
<input class="fast-action" data-mins="0" type="button" value="Vypnout hned">
</div>
<div>
<h2>Výjimky</h2>
<div id="overrides"></div>
<form id="add-override">
<label for="override-action">Akce:</label>
<select id="override-action">
<option value="allow">Povolit</option>
<option value="deny">Zakázat</option>
</select><br>
<label for="override-start">Od:</label>
<input id="override-start" type="datetime-local"><br>
<label for="override-end">Do:</label>
<input id="override-end" type="datetime-local"><br>
<input type="submit" value="Přidat">
</form>
</div>
<div>
<h2>Plán</h2>
<div id="schedules">
<span>Pondělí:</span><span id="schedule-0"></span>
<span>Úterý:</span><span id="schedule-1"></span>
<span>Středa:</span><span id="schedule-2"></span>
<span>Čtvrtek:</span><span id="schedule-3"></span>
<span>Pátek:</span><span id="schedule-4"></span>
<span>Sobota:</span><span id="schedule-5"></span>
<span>Neděle:</span><span id="schedule-6"></span>
</div>
<form id="update-schedule">
<label for="schedule-action">Akce:</label>
<select id="schedule-action">
<option value="allow">Povolit</option>
<option value="deny">Zakázat</option>
</select>
<label for="schedule-days">Den:</label>
<select id="schedule-days" size="7" multiple>
<option value="0">Pondělí</option>
<option value="1">Úterý</option>
<option value="2">Středa</option>
<option value="3">Čtvrtek</option>
<option value="4">Pátek</option>
<option value="5">Sobota</option>
<option value="6">Neděle</option>
</select>
<label for="schedule-start">Od:</label>
<input id="schedule-start" type="time">
<label for="schedule-end">Do:</label>
<input id="schedule-end" type="time">
<input type="submit" value="Přidat">
</form>
</div>
</body>
</html>

18
pyproject.toml Normal file
View File

@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "parental-control"
version = "0.0.1"
authors = [
{name = "Disassembler", email = "disassembler@dasm.cz"},
]
requires-python = ">=3.6"
license = {text = "MIT"}
dependencies = [
"flask",
]
[tool.setuptools]
packages = ["api"]

122
static/script.js Normal file
View File

@ -0,0 +1,122 @@
function humanReadableDate(str) {
let date = new Date();
let today = date.toISOString().split("T")[0];
date.setDate(date.getDate() - 1);
let yesterday = date.toISOString().split("T")[0];
date.setDate(date.getDate() + 2);
let tomorrow = date.toISOString().split("T")[0];
return str.replace("T", " ").replaceAll(yesterday, "včera").replaceAll(today, "dnes").replaceAll(tomorrow, "zítra");
}
function refreshStatus(data) {
let status = document.getElementById("status");
let action, then_action;
if (data["action"]) {
action = "Povoleno"
then_action = "zakázáno"
} else {
action = "Zakázáno"
then_action = "povoleno"
}
let text = action;
if (data["until"]) {
text += " do " + humanReadableDate(data["until"]) + ", pak " + then_action;
if (data["then_until"]) {
text += " do " + humanReadableDate(data["then_until"]);
} else {
text += " napořád"
}
} else {
text += " napořád"
}
status.textContent = text;
}
function refreshOverrides(data) {
let overrides = document.getElementById("overrides");
overrides.innerHTML = "";
for (const record of data) {
let overrideText = document.createElement("span");
overrideText.textContent = (record["action"] ? "Povoleno" : "Zakázáno") + " od " + humanReadableDate(record["start"]) + " do " + humanReadableDate(record["end"]);
let overrideButton = document.createElement("input");
overrideButton.type = "button";
overrideButton.value = "❌";
overrideButton.addEventListener("click", async () => {
let response = await fetch("api/delete-override", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
action: record["action"],
start: record["start"],
end: record["end"]
}),
});
await refresh(await response.json());
})
overrides.appendChild(overrideText);
overrides.appendChild(overrideButton);
}
}
function refreshSchedules(data) {
for (const [day, entries] of Object.entries(data)) {
let schedule = "Zakázáno"
if (entries.length > 0) {
schedule = entries.map((entry) => entry["start"] + " - " + entry["end"]).join(", ");
}
document.getElementById("schedule-" + day).textContent = schedule;
}
}
async function refresh(data) {
data = data || await (await fetch("api/data")).json();
refreshStatus(data["status"]);
refreshOverrides(data["overrides"]);
refreshSchedules(data["schedules"]);
}
async function addOverride(e) {
e.preventDefault();
let response = await fetch("api/override", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
action: document.getElementById("override-action").value == "allow",
start: document.getElementById("override-start").value,
end: document.getElementById("override-end").value,
}),
});
await refresh(await response.json());
document.getElementById("add-override").reset();
}
async function updateSchedule(e) {
e.preventDefault();
let response = await fetch("api/schedule", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
action: document.getElementById("schedule-action").value == "allow",
days: Array.from(document.getElementById("schedule-days").selectedOptions).map(v => parseInt(v.value)),
start: document.getElementById("schedule-start").value,
end: document.getElementById("schedule-end").value
})
});
await refresh(await response.json());
document.getElementById("update-schedule").reset();
}
async function fastAction(e) {
let response = await fetch("api/fast-action/" + e.target.dataset.mins);
await refresh(await response.json());
}
window.addEventListener("DOMContentLoaded", () => {
refresh();
document.getElementById("add-override").addEventListener("submit", addOverride);
document.getElementById("update-schedule").addEventListener("submit", updateSchedule);
Array.from(document.getElementsByClassName("fast-action")).forEach(function(element) {
element.addEventListener("click", fastAction);
});
window.setInterval(refresh, 5000);
});

65
static/style.css Normal file
View File

@ -0,0 +1,65 @@
body {
max-width: 500px;
font-family: serif;
}
h2 {
margin: 1em 0 0.5em;
}
form {
display: grid;
padding: 0 1em 1em 1em;
background: #eee;
border: 1px solid #ccc;
margin-top: 1em;
}
input, select {
background: #fff;
border: 1px solid #999;
padding: 0.5em;
}
input[type="button"] {
background: #eee;
}
input[type="button"]:hover {
background: #ddd;
}
input[type="submit"] {
margin-top: 0.5em;
}
input[type="submit"]:hover {
background: #f6f6f6;
}
label {
padding: 0.5em 0.5em 0.5em 0;
}
#status {
margin-bottom: 1em;
}
#overrides {
display: grid;
grid-template-columns: 1fr 30px;
margin-top: -0.4em;
}
#schedules {
display: grid;
grid-template-columns: 5em 1fr;
margin-top: -0.4em;
}
#overrides span, #schedules span {
margin: 0.5em 0;
}
@media (min-width: 400px) {
form {
grid-template-columns: 50px 1fr;
grid-gap: 2px;
padding: 1em;
}
form label {
grid-column: 1 / 2;
text-align: right;
}
form input, form select {
grid-column: 2 / 3;
}
}

0
tests/__init__.py Normal file
View File

389
tests/test_app.py Normal file
View File

@ -0,0 +1,389 @@
from datetime import datetime
from unittest.mock import patch
import pytest
from parental_control import DATETIME_FORMAT, app
from parental_control.span import DateTimeSpan, MinuteSpan
@pytest.mark.parametrize(
"point_in_time, schedules, overrides, expected_result",
[
# Point in time within override
(
"2023-08-24 14:00",
{day: [] for day in range(7)},
[{"start": "2023-08-24 12:00", "end": "2023-08-24 18:00", "action": True}],
True,
),
# Point in time within schedule
(
"2023-08-24 14:00",
{3: [{"start": "10:00", "end": "16:00"}]},
[],
True,
),
# Point in time outside schedule and override
(
"2023-08-24 08:00",
{3: [{"start": "10:00", "end": "16:00"}]},
[{"start": "2023-08-24 12:00", "end": "2023-08-24 18:00", "action": True}],
False,
),
],
)
def test_get_action_at(point_in_time, schedules, overrides, expected_result):
point_in_time = datetime.strptime(point_in_time, DATETIME_FORMAT)
schedules = {
day: [MinuteSpan.from_args(**span) for span in schedule]
for day, schedule in schedules.items()
}
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
result = app.get_action_at(point_in_time, schedules, overrides)
assert result == expected_result
@pytest.mark.parametrize(
"point_in_time, action, schedules, overrides, expected_result",
[
# No schedule, no overrides, next change will never occur
(
"2023-08-24 08:00",
False,
{day: [] for day in range(7)},
[],
None,
),
# Schedule 24/7, no overrides, next change will never occur
(
"2023-08-24 08:00",
True,
{day: [{"start": "00:00", "end": "24:00"}] for day in range(7)},
[],
None,
),
# No schedule, next change occurs after allow override starts
(
"2023-08-24 08:00",
False,
{day: [] for day in range(7)},
[{"start": "2023-08-26 10:00", "end": "2023-08-26 14:00", "action": True}],
"2023-08-26 10:00",
),
# Schedule 24/7, next change occurs after deny override starts
(
"2023-08-24 08:00",
True,
{day: [{"start": "00:00", "end": "24:00"}] for day in range(7)},
[{"start": "2023-08-26 10:00", "end": "2023-08-26 14:00", "action": False}],
"2023-08-26 10:00",
),
# No schedule, expired overrides, next change will never occur
(
"2023-08-24 08:00",
False,
{day: [] for day in range(7)},
[{"start": "2023-08-23 10:00", "end": "2023-08-23 14:00", "action": True}],
None,
),
# No schedule, next change occurs after allow override expires
(
"2023-08-24 08:00",
True,
{day: [] for day in range(7)},
[{"start": "2023-08-24 08:00", "end": "2023-08-26 14:00", "action": True}],
"2023-08-26 14:00",
),
# Next change occurs after allow override expires
(
"2023-08-24 08:00",
True,
{day: [{"start": "14:00", "end": "16:00"}] for day in range(7)},
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
"2023-08-24 10:00",
),
# Next change occurs after schedule span ends
(
"2023-08-24 14:00",
True,
{
day: [{"start": "08:00", "end": "10:00"}, {"start": "14:00", "end": "16:00"}]
for day in range(7)
},
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
"2023-08-24 16:00",
),
# Next change occurs after schedule span starts
(
"2023-08-24 13:00",
False,
{
day: [{"start": "08:00", "end": "10:00"}, {"start": "14:00", "end": "16:00"}]
for day in range(7)
},
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
"2023-08-24 14:00",
),
# Next change occurs after deny override starts
(
"2023-08-24 11:00",
True,
{day: [{"start": "10:00", "end": "16:00"}] for day in range(7)},
[{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": False}],
"2023-08-24 12:00",
),
# Next change occurs after allow override starts
(
"2023-08-24 10:00",
False,
{day: [{"start": "16:00", "end": "18:00"}] for day in range(7)},
[{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": True}],
"2023-08-24 12:00",
),
# Next change occurs after schedule span ends after 7 days
(
"2023-08-21 13:00",
True,
{
0: [{"start": "00:00", "end": "04:00"}, {"start": "10:00", "end": "24:00"}],
1: [{"start": "00:00", "end": "24:00"}],
2: [{"start": "00:00", "end": "24:00"}],
3: [{"start": "00:00", "end": "24:00"}],
4: [{"start": "00:00", "end": "24:00"}],
5: [{"start": "00:00", "end": "24:00"}],
6: [{"start": "00:00", "end": "24:00"}],
},
[{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": True}],
"2023-08-28 04:00",
),
# Next change occurs after schedule span starts after 7 days
(
"2023-08-21 13:00",
False,
{
0: [{"start": "00:00", "end": "04:00"}],
1: [],
2: [],
3: [],
4: [],
5: [],
6: [],
},
[{"start": "2023-08-31 12:00", "end": "2023-08-31 14:00", "action": True}],
"2023-08-28 00:00",
),
],
)
def test_get_next_action_change(point_in_time, action, schedules, overrides, expected_result):
point_in_time = datetime.strptime(point_in_time, DATETIME_FORMAT)
schedules = {
day: [MinuteSpan.from_args(**span) for span in schedule]
for day, schedule in schedules.items()
}
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
result = app.get_next_action_change(point_in_time, action, schedules, overrides)
if expected_result:
expected_result = datetime.strptime(expected_result, DATETIME_FORMAT)
assert result == expected_result
@pytest.mark.parametrize(
"schedules, overrides, expected_result",
[
(
{day: [] for day in range(7)},
[],
{"action": False, "until": "", "then_until": ""},
),
(
{day: [{"start": "14:00", "end": "16:00"}] for day in range(7)},
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
{"action": False, "until": "2023-08-24 14:00", "then_until": "2023-08-24 16:00"},
),
(
{day: [] for day in range(7)},
[{"start": "2023-08-24 08:00", "end": "2023-08-24 12:00", "action": True}],
{"action": True, "until": "2023-08-24 12:00", "then_until": ""},
),
],
)
@patch("parental_control.app.datetime", wraps=datetime)
def test_calculate_status(mock_datetime, schedules, overrides, expected_result):
mock_datetime.now.return_value = datetime(2023, 8, 24, 11)
schedules = {
day: [MinuteSpan.from_args(**span) for span in schedule]
for day, schedule in schedules.items()
}
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
result = app.calculate_status(schedules, overrides)
assert result == expected_result
@pytest.mark.parametrize(
"schedule, span, action, expected_result",
[
# Allow span in empty schedule
(
[],
{"start": "09:00", "end": "11:00"},
True,
[{"start": "9:00", "end": "11:00"}],
),
# Allow span same as already existing span
(
[{"start": "9:00", "end": "11:00"}],
{"start": "09:00", "end": "11:00"},
True,
[{"start": "9:00", "end": "11:00"}],
),
# Allow span (starting at 0:00) prepening it before existing span
(
[{"start": "9:00", "end": "11:00"}],
{"start": "00:00", "end": "05:00"},
True,
[{"start": "0:00", "end": "5:00"}, {"start": "9:00", "end": "11:00"}],
),
# Allow span (ending at 24:00) appending it after existing span
(
[{"start": "9:00", "end": "11:00"}],
{"start": "19:00", "end": "0:00"},
True,
[{"start": "9:00", "end": "11:00"}, {"start": "19:00", "end": "24:00"}],
),
# Allow span merging existing spans together
(
[{"start": "9:00", "end": "11:00"}, {"start": "14:00", "end": "16:00"}],
{"start": "11:00", "end": "14:00"},
True,
[{"start": "9:00", "end": "16:00"}],
),
# Deny span from empty schedule
(
[],
{"start": "12:00", "end": "14:00"},
False,
[],
),
# Deny span which is already denied
(
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
{"start": "12:00", "end": "14:00"},
False,
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
),
# Deny span removing existing allow spans
(
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
{"start": "0:00", "end": "0:00"},
False,
[],
),
# Deny span within allow span, splitting the allow span
(
[{"start": "9:00", "end": "16:00"}],
{"start": "11:00", "end": "14:00"},
False,
[{"start": "9:00", "end": "11:00"}, {"start": "14:00", "end": "16:00"}],
),
# Deny span moving end and start of existing allow spans
(
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
{"start": "10:00", "end": "17:00"},
False,
[{"start": "9:00", "end": "10:00"}, {"start": "17:00", "end": "18:00"}],
),
],
)
def test_update_schedule(schedule, span, action, expected_result):
schedule = [MinuteSpan.from_args(**s) for s in schedule]
span = MinuteSpan.from_args(**span)
app.update_schedule(schedule, span, action)
expected_result = [MinuteSpan.from_args(**s) for s in expected_result]
assert schedule == expected_result
@pytest.mark.parametrize(
"overrides, start, end, action, expected_result",
[
# Add override into empty list of overrides
(
[],
{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
),
# Add override into list of overrides
(
[
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
],
{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
[
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
],
),
# Add override removing another override spanning over smaller period
(
[
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
],
{"start": "2023-08-24 15:00", "end": "2023-08-24 22:00", "action": False},
[
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
{"start": "2023-08-24 15:00", "end": "2023-08-24 22:00", "action": False},
],
),
# Add override within existing override span, splitting it
(
[{"start": "2023-08-24 08:00", "end": "2023-08-24 20:00", "action": False}],
{"start": "2023-08-24 16:00", "end": "2023-08-24 18:00", "action": True},
[
{"start": "2023-08-24 08:00", "end": "2023-08-24 16:00", "action": False},
{"start": "2023-08-24 16:00", "end": "2023-08-24 18:00", "action": True},
{"start": "2023-08-24 18:00", "end": "2023-08-24 20:00", "action": False},
],
),
# Add override moving end and start of existing overrides
(
[
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
],
{"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": False},
[
{"start": "2023-08-24 00:00", "end": "2023-08-24 04:00", "action": True},
{"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": False},
{"start": "2023-08-24 18:00", "end": "2023-08-24 20:00", "action": True},
],
),
# Add override merging existing overrides
(
[
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
],
{"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": True},
[{"start": "2023-08-24 00:00", "end": "2023-08-24 20:00", "action": True}],
),
],
)
def test_update_overrides(overrides, span, expected_result):
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
span = DateTimeSpan.from_args(**span)
app.update_overrides(overrides, span)
expected_result = [DateTimeSpan.from_args(**o) for o in expected_result]
assert overrides == expected_result

17
tox.ini Normal file
View File

@ -0,0 +1,17 @@
[coverage:run]
branch = True
[tox]
envlist = pylint, pytest, black
[testenv:pylint]
deps = pylint
commands = pylint parental_control tests {posargs}
[testenv:pytest]
deps = pytest-cov
commands = pytest tests -vv --cov parental_control --cov-report term --cov-report xml --cov-fail-under 100 {posargs}
[testenv:black]
deps = black
commands = black -l 100 --check --diff parental_control tests {posargs}

3
wsgi.py Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/python3
from api.app import app as application