aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2022-11-26 17:21:33 +0800
committercrupest <crupest@outlook.com>2022-11-26 17:21:33 +0800
commit80bb178ab3234fb7b883c6489ff3c5e2631e208e (patch)
tree588d8efd20f1e427a9c72ce8fadd8a5dcb0b4879
parent63e43d1a9b880c92d73b7ec97b596c38d3e758e2 (diff)
downloadcrupest-80bb178ab3234fb7b883c6489ff3c5e2631e208e.tar.gz
crupest-80bb178ab3234fb7b883c6489ff3c5e2631e208e.tar.bz2
crupest-80bb178ab3234fb7b883c6489ff3c5e2631e208e.zip
Add crupest-api.
-rw-r--r--.gitignore2
-rw-r--r--docker/auto-backup/AutoBackup/.dockerignore2
-rw-r--r--docker/crupest-api/CrupestApi/.dockerignore2
-rw-r--r--docker/crupest-api/CrupestApi/.gitignore2
-rw-r--r--docker/crupest-api/CrupestApi/Config/TodosConfiguration.cs15
-rw-r--r--docker/crupest-api/CrupestApi/CrupestApi.csproj8
-rw-r--r--docker/crupest-api/CrupestApi/Program.cs160
-rw-r--r--docker/crupest-api/CrupestApi/Properties/launchSettings.json13
-rw-r--r--docker/crupest-api/CrupestApi/appsettings.json8
-rw-r--r--docker/crupest-api/Dockerfile13
-rw-r--r--template/crupest-api-config.json.template8
-rw-r--r--template/nginx/reverse-proxy.conf.template1
-rw-r--r--template/nginx/root.conf.template15
-rwxr-xr-xtool/aio.py79
-rw-r--r--tool/modules/config.py75
-rw-r--r--tool/modules/path.py6
-rwxr-xr-xtool/test-crupest-api.py80
17 files changed, 426 insertions, 63 deletions
diff --git a/.gitignore b/.gitignore
index c7fcf38..4f5cc0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
docker-compose.yaml
mailserver.env
+crupest-api-config.json
data
nginx-config
+log
__pycache__
diff --git a/docker/auto-backup/AutoBackup/.dockerignore b/docker/auto-backup/AutoBackup/.dockerignore
new file mode 100644
index 0000000..7de5508
--- /dev/null
+++ b/docker/auto-backup/AutoBackup/.dockerignore
@@ -0,0 +1,2 @@
+obj
+bin
diff --git a/docker/crupest-api/CrupestApi/.dockerignore b/docker/crupest-api/CrupestApi/.dockerignore
new file mode 100644
index 0000000..7de5508
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/.dockerignore
@@ -0,0 +1,2 @@
+obj
+bin
diff --git a/docker/crupest-api/CrupestApi/.gitignore b/docker/crupest-api/CrupestApi/.gitignore
new file mode 100644
index 0000000..7de5508
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/.gitignore
@@ -0,0 +1,2 @@
+obj
+bin
diff --git a/docker/crupest-api/CrupestApi/Config/TodosConfiguration.cs b/docker/crupest-api/CrupestApi/Config/TodosConfiguration.cs
new file mode 100644
index 0000000..68f893a
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/Config/TodosConfiguration.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CrupestApi.Config
+{
+ public class TodoConfiguration
+ {
+ [Required]
+ public string Username { get; set; } = default!;
+ [Required]
+ public int ProjectNumber { get; set; } = default!;
+ [Required]
+ public string Token { get; set; } = default!;
+ public int? Count { get; set; }
+ }
+} \ No newline at end of file
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.csproj b/docker/crupest-api/CrupestApi/CrupestApi.csproj
new file mode 100644
index 0000000..4348979
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/CrupestApi.csproj
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+</Project>
diff --git a/docker/crupest-api/CrupestApi/Program.cs b/docker/crupest-api/CrupestApi/Program.cs
new file mode 100644
index 0000000..c62bf4d
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/Program.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.AspNetCore.Http;
+using CrupestApi.Config;
+
+public class TodoItem
+{
+ public string Status { get; set; } = default!;
+ public string Title { get; set; } = default!;
+}
+
+internal class Program
+{
+ private static void Main(string[] args)
+ {
+ using var httpClient = new HttpClient();
+
+ var builder = WebApplication.CreateBuilder(args);
+
+ string configFilePath = Environment.GetEnvironmentVariable("CRUPEST_API_CONFIG_FILE") ?? "/config.json";
+
+ builder.Configuration.AddJsonFile(configFilePath, optional: false, reloadOnChange: true);
+
+ string? logFilePath = Environment.GetEnvironmentVariable("CRUPEST_API_LOG_FILE");
+ if (logFilePath is not null)
+ {
+ // TODO: Log to file.
+ builder.Logging.AddSimpleConsole(logger =>
+ {
+ logger.ColorBehavior = LoggerColorBehavior.Disabled;
+ });
+ }
+
+ var app = builder.Build();
+
+ app.MapGet("/api/todos", async ([FromServices] IConfiguration configuration, [FromServices] ILoggerFactory loggerFactory) =>
+ {
+ var logger = loggerFactory.CreateLogger("CrupestApi.Todos");
+
+ static string CreateGraphQLQuery(TodoConfiguration todoConfiguration)
+ {
+ return $$"""
+{
+ user(login: "{{todoConfiguration.Username}}") {
+ projectV2(number: {{todoConfiguration.ProjectNumber}}) {
+ items(last: {{todoConfiguration.Count ?? 20}}) {
+ nodes {
+ __typename
+ content {
+ __typename
+ ... on Issue {
+ title
+ closed
+ }
+ ... on PullRequest {
+ title
+ closed
+ }
+ ... on DraftIssue {
+ title
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+""";
+ }
+
+ var todoConfiguration = configuration.GetSection("Todos").Get<TodoConfiguration>();
+ if (todoConfiguration is null)
+ {
+ throw new Exception("Fail to get todos configuration.");
+ }
+
+ using var requestContent = new StringContent(JsonSerializer.Serialize(new
+ {
+ query = CreateGraphQLQuery(todoConfiguration)
+ }));
+ requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Json, Encoding.UTF8.WebName);
+
+ using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.github.com/graphql");
+ request.Content = requestContent;
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", todoConfiguration.Token);
+
+ using var response = await httpClient.SendAsync(request);
+ var responseBody = await response.Content.ReadAsStringAsync();
+ logger.LogInformation(response.StatusCode.ToString());
+ logger.LogInformation(responseBody);
+
+
+ if (response.IsSuccessStatusCode)
+ {
+ using var responseJson = JsonSerializer.Deserialize<JsonDocument>(responseBody);
+ if (responseJson is null)
+ {
+ throw new Exception("Fail to deserialize response body.");
+ }
+
+ var nodes = responseJson.RootElement.GetProperty("data").GetProperty("user").GetProperty("projectV2").GetProperty("items").GetProperty("nodes").EnumerateArray();
+
+ var result = new List<TodoItem>();
+
+ foreach (var node in nodes)
+ {
+ var content = node.GetProperty("content");
+ var title = content.GetProperty("title").GetString();
+ if (title is null)
+ {
+ throw new Exception("Fail to get title.");
+ }
+ JsonElement closedElement;
+ bool closed;
+ if (content.TryGetProperty("closed", out closedElement))
+ {
+ closed = closedElement.GetBoolean();
+ }
+ else
+ {
+ closed = false;
+ }
+
+ result.Add(new TodoItem
+ {
+ Title = title,
+ Status = closed ? "Done" : "Todo"
+ });
+ }
+
+ return Results.Json(result, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ }, statusCode: 200);
+ }
+ else
+ {
+ const string message = "Fail to get todos from GitHub.";
+ logger.LogError(message);
+
+ return Results.Json(new
+ {
+ message
+ }, statusCode: StatusCodes.Status503ServiceUnavailable);
+ }
+ });
+
+ app.Run();
+ }
+} \ No newline at end of file
diff --git a/docker/crupest-api/CrupestApi/Properties/launchSettings.json b/docker/crupest-api/CrupestApi/Properties/launchSettings.json
new file mode 100644
index 0000000..01f2a12
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "dev": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:5188",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/docker/crupest-api/CrupestApi/appsettings.json b/docker/crupest-api/CrupestApi/appsettings.json
new file mode 100644
index 0000000..53753bd
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+} \ No newline at end of file
diff --git a/docker/crupest-api/Dockerfile b/docker/crupest-api/Dockerfile
new file mode 100644
index 0000000..ba7d75b
--- /dev/null
+++ b/docker/crupest-api/Dockerfile
@@ -0,0 +1,13 @@
+FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
+COPY CrupestApi /CrupestApi
+WORKDIR /CrupestApi
+RUN dotnet publish CrupestApi.csproj --configuration Release --output ./publish/ -r linux-x64 --self-contained false
+
+FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine
+ENV ASPNETCORE_URLS=http://0.0.0.0:5000
+ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
+COPY --from=build /CrupestApi/publish /CrupestApi
+WORKDIR /CrupestApi
+VOLUME [ "/config.json" ]
+EXPOSE 5000
+ENTRYPOINT ["dotnet", "CrupestApi.dll"]
diff --git a/template/crupest-api-config.json.template b/template/crupest-api-config.json.template
new file mode 100644
index 0000000..fc5eb85
--- /dev/null
+++ b/template/crupest-api-config.json.template
@@ -0,0 +1,8 @@
+{
+ "Todos": {
+ "Username": "$CRUPEST_GITHUB_USERNAME",
+ "ProjectNumber": "$CRUPEST_GITHUB_PROJECT_NUMBER",
+ "Token": "$CRUPEST_GITHUB_TOKEN",
+ "Count": "$CRUPEST_GITHUB_TODO_COUNT"
+ }
+}
diff --git a/template/nginx/reverse-proxy.conf.template b/template/nginx/reverse-proxy.conf.template
index 81ba1e9..99184e0 100644
--- a/template/nginx/reverse-proxy.conf.template
+++ b/template/nginx/reverse-proxy.conf.template
@@ -8,7 +8,6 @@ server {
server_name ${CRUPEST_NGINX_SUBDOMAIN}.${CRUPEST_DOMAIN};
location / {
- proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
diff --git a/template/nginx/root.conf.template b/template/nginx/root.conf.template
index 8214bf7..110def4 100644
--- a/template/nginx/root.conf.template
+++ b/template/nginx/root.conf.template
@@ -1,9 +1,24 @@
+upstream crupest-api {
+ server crupest-api:5000
+}
+
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${CRUPEST_DOMAIN};
root /srv/www;
+
+ location /api {
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_pass http://crupest-api;
+ }
}
server {
diff --git a/tool/aio.py b/tool/aio.py
index 8ccaab9..5498141 100755
--- a/tool/aio.py
+++ b/tool/aio.py
@@ -2,8 +2,6 @@
import os
import os.path
-import pwd
-import grp
import sys
import argparse
import shutil
@@ -14,6 +12,7 @@ from modules.path import *
from modules.template import Template
from modules.nginx import *
from modules.configfile import *
+from modules.config import *
console = Console()
@@ -248,57 +247,6 @@ console.print(
for filename in template_name_list:
console.print(f"- [magenta]{filename}.template[/]")
-
-class ConfigVar:
- def __init__(self, name: str, description: str, default_value_generator, /, default_value_for_ask=None):
- """Create a config var.
-
- Args:
- name (str): The name of the config var.
- description (str): The description of the config var.
- default_value_generator (typing.Callable([], str) | str): The default value generator of the config var. If it is a string, it will be used as the input prompt and let user input the value.
- """
- self.name = name
- self.description = description
- self.default_value_generator = default_value_generator
- self.default_value_for_ask = default_value_for_ask
-
- def get_default_value(self):
- if isinstance(self.default_value_generator, str):
- return Prompt.ask(self.default_value_generator, console=console, default=self.default_value_for_ask)
- else:
- return self.default_value_generator()
-
-
-config_var_list: list = [
- ConfigVar("CRUPEST_DOMAIN", "domain name",
- "Please input your domain name"),
- ConfigVar("CRUPEST_EMAIL", "admin email address",
- "Please input your email address"),
- ConfigVar("CRUPEST_USER", "your system account username",
- lambda: pwd.getpwuid(os.getuid()).pw_name),
- ConfigVar("CRUPEST_GROUP", "your system account group name",
- lambda: grp.getgrgid(os.getgid()).gr_name),
- ConfigVar("CRUPEST_UID", "your system account uid",
- lambda: str(os.getuid())),
- ConfigVar("CRUPEST_GID", "your system account gid",
- lambda: str(os.getgid())),
- ConfigVar("CRUPEST_HALO_DB_PASSWORD",
- "password for halo h2 database, once used never change it", lambda: os.urandom(8).hex()),
- ConfigVar("CRUPEST_IN_CHINA",
- "set to true if you are in China, some network optimization will be applied", lambda: "false"),
- ConfigVar("CRUPEST_AUTO_BACKUP_COS_SECRET_ID",
- "access key id for Tencent COS, used for auto backup", "Please input your Tencent COS access key id for backup"),
- ConfigVar("CRUPEST_AUTO_BACKUP_COS_SECRET_KEY",
- "access key secret for Tencent COS, used for auto backup", "Please input your Tencent COS access key for backup"),
- ConfigVar("CRUPEST_AUTO_BACKUP_COS_REGION",
- "region for Tencent COS, used for auto backup", "Please input your Tencent COS region for backup", "ap-hongkong"),
- ConfigVar("CRUPEST_AUTO_BACKUP_BUCKET_NAME",
- "bucket name for Tencent COS, used for auto backup", "Please input your Tencent COS bucket name for backup")
-]
-
-config_var_name_set = set([config_var.name for config_var in config_var_list])
-
template_list: list = []
config_var_name_set_in_template = set()
for template_path in os.listdir(template_dir):
@@ -315,17 +263,24 @@ for key in config_var_name_set_in_template:
console.print("")
# check vars
-if not config_var_name_set_in_template == config_var_name_set:
- console.print(
- "The variables needed in templates are not same to the explicitly declared ones! There must be something wrong.", style="red")
- console.print("The explicitly declared ones are:")
- for key in config_var_name_set:
- console.print(key, end=" ", style="magenta")
+check_success, more, less = check_config_var_set(
+ config_var_name_set_in_template)
+if len(more) != 0:
+ console.print("There are more variables in templates than in config file:",
+ style="red")
+ for key in more:
+ console.print(key, style="magenta")
+if len(less) != 0:
+ console.print("However, following config vars are not used:",
+ style="yellow")
+ for key in less:
+ console.print(key, style="magenta")
+
+if not check_success:
console.print(
- "\nTry to check template files and edit the var list at the head of this script. Aborted! See you next time!")
+ "Please check you config vars and make sure the needed ones are defined!", style="red")
exit(1)
-
console.print("Now let's check if they are already generated...")
conflict = False
@@ -388,7 +343,7 @@ else:
console.print(
"Oops! It seems you have missed some keys in your config file. Let's add them!", style="green")
for config_var in missed_config_vars:
- config[config_var.name] = config_var.get_default_value()
+ config[config_var.name] = config_var.get_default_value(console)
content = config_to_str(config)
with open(config_file_path, "w") as f:
f.write(content)
diff --git a/tool/modules/config.py b/tool/modules/config.py
new file mode 100644
index 0000000..37ad996
--- /dev/null
+++ b/tool/modules/config.py
@@ -0,0 +1,75 @@
+from rich.prompt import Prompt
+import pwd
+import grp
+import os
+
+
+class ConfigVar:
+ def __init__(self, name: str, description: str, default_value_generator, /, default_value_for_ask=None):
+ """Create a config var.
+
+ Args:
+ name (str): The name of the config var.
+ description (str): The description of the config var.
+ default_value_generator (typing.Callable([], str) | str): The default value generator of the config var. If it is a string, it will be used as the input prompt and let user input the value.
+ """
+ self.name = name
+ self.description = description
+ self.default_value_generator = default_value_generator
+ self.default_value_for_ask = default_value_for_ask
+
+ def get_default_value(self, /, console):
+ if isinstance(self.default_value_generator, str):
+ return Prompt.ask(self.default_value_generator, console=console, default=self.default_value_for_ask)
+ else:
+ return self.default_value_generator()
+
+
+config_var_list: list = [
+ ConfigVar("CRUPEST_DOMAIN", "domain name",
+ "Please input your domain name"),
+ ConfigVar("CRUPEST_EMAIL", "admin email address",
+ "Please input your email address"),
+ ConfigVar("CRUPEST_USER", "your system account username",
+ lambda: pwd.getpwuid(os.getuid()).pw_name),
+ ConfigVar("CRUPEST_GROUP", "your system account group name",
+ lambda: grp.getgrgid(os.getgid()).gr_name),
+ ConfigVar("CRUPEST_UID", "your system account uid",
+ lambda: str(os.getuid())),
+ ConfigVar("CRUPEST_GID", "your system account gid",
+ lambda: str(os.getgid())),
+ ConfigVar("CRUPEST_HALO_DB_PASSWORD",
+ "password for halo h2 database, once used never change it", lambda: os.urandom(8).hex()),
+ ConfigVar("CRUPEST_IN_CHINA",
+ "set to true if you are in China, some network optimization will be applied", lambda: "false"),
+ ConfigVar("CRUPEST_AUTO_BACKUP_COS_SECRET_ID",
+ "access key id for Tencent COS, used for auto backup", "Please input your Tencent COS access key id for backup"),
+ ConfigVar("CRUPEST_AUTO_BACKUP_COS_SECRET_KEY",
+ "access key secret for Tencent COS, used for auto backup", "Please input your Tencent COS access key for backup"),
+ ConfigVar("CRUPEST_AUTO_BACKUP_COS_REGION",
+ "region for Tencent COS, used for auto backup", "Please input your Tencent COS region for backup", "ap-hongkong"),
+ ConfigVar("CRUPEST_AUTO_BACKUP_BUCKET_NAME",
+ "bucket name for Tencent COS, used for auto backup", "Please input your Tencent COS bucket name for backup"),
+ ConfigVar("CRUPEST_GITHUB_USERNAME",
+ "github username for fetching todos", "Please input your github username for fetching todos", "crupest"),
+ ConfigVar("CRUPEST_GITHUB_PROJECT_NUMBER",
+ "github project number for fetching todos", "Please input your github project number for fetching todos", "2"),
+ ConfigVar("CRUPEST_GITHUB_TOKEN",
+ "github token for fetching todos", "Please input your github token for fetching todos"),
+ ConfigVar("CRUPEST_GITHUB_TODO_COUNT",
+ "github todo count", "Please input your github todo count", 10),
+]
+
+config_var_name_set = set([config_var.name for config_var in config_var_list])
+
+
+def check_config_var_set(needed_config_var_set: set):
+ more = []
+ less = []
+ for var_name in needed_config_var_set:
+ if var_name not in config_var_name_set:
+ more.append(var_name)
+ for var_name in config_var_name_set:
+ if var_name not in needed_config_var_set:
+ less.append(var_name)
+ return (True if len(more) == 0 else False, more, less)
diff --git a/tool/modules/path.py b/tool/modules/path.py
index 5d9aa3a..c4cf35b 100644
--- a/tool/modules/path.py
+++ b/tool/modules/path.py
@@ -1,3 +1,4 @@
+import os
import os.path
script_dir = os.path.relpath(os.path.dirname(__file__))
@@ -9,3 +10,8 @@ data_dir = os.path.join(project_dir, "data")
tool_dir = os.path.join(project_dir, "tool")
config_file_path = os.path.join(data_dir, "config")
nginx_config_dir = os.path.join(project_dir, "nginx-config")
+log_dir = os.path.join(project_dir, "log")
+
+def ensure_log_dir():
+ if not os.path.exists(log_dir):
+ os.mkdir(log_dir)
diff --git a/tool/test-crupest-api.py b/tool/test-crupest-api.py
new file mode 100755
index 0000000..5cd461d
--- /dev/null
+++ b/tool/test-crupest-api.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+
+import subprocess
+import os
+import sys
+from os.path import *
+import signal
+from modules.path import *
+import time
+from rich.console import Console
+from urllib.request import urlopen
+from http.client import *
+import json
+
+console = console = Console()
+
+ensure_log_dir()
+
+dotnet_project = join(project_dir, "docker", "crupest-api", "CrupestApi")
+dotnet_log_path = abspath(join(log_dir, "crupest-api-log"))
+dotnet_config_path = abspath(join(project_dir, "crupest-api-config.json"))
+
+os.environ["CRUPEST_API_CONFIG_FILE"] = dotnet_log_path
+os.environ["CRUPEST_API_LOG_FILE"] = dotnet_config_path
+
+popen = subprocess.Popen(
+ ["dotnet", "run", "--project", dotnet_project, "--launch-profile", "dev"]
+)
+
+console.print("Sleep for 3s to wait for server startup.")
+time.sleep(3)
+
+
+def do_the_test():
+ res: HTTPResponse = urlopen("http://localhost:5188/api/todos")
+ console.print(res)
+ body = res.read()
+ console.print(body)
+
+ if res.status != 200:
+ raise Exception("Status code is not 200.")
+ result = json.load(body)
+ if not isinstance(result, list):
+ raise Exception("Result is not an array.")
+ if len(result) == 0:
+ raise Exception("Result is an empty array.")
+ if not isinstance(result[0], dict):
+ raise Exception("Result[0] is not an object.")
+ if not isinstance(result[0].get("title"), str):
+ raise Exception("Result[0].title is not a string.")
+ if not isinstance(result[0].get("status"), str):
+ raise Exception("Result[0].status is not a string.")
+
+
+for i in range(0, 2):
+ console.print(f"Test begin with attempt {i + 1}", style="cyan")
+ try:
+ do_the_test()
+ console.print("Test passed.", style="green")
+ popen.send_signal(signal.SIGTERM)
+ popen.wait()
+ exit(0)
+ except Exception as e:
+ console.print(e)
+ console.print(
+ "Test failed. Try again after sleep for 1s.", style="red")
+ time.sleep(1)
+
+try:
+ console.print(
+ f"Test begin with attempt {i + 2}, also the final one.", style="cyan")
+ do_the_test()
+ console.print("Test passed.", style="green")
+ popen.send_signal(signal.SIGTERM)
+ popen.wait()
+ exit(0)
+except Exception as e:
+ console.print(e)
+ console.print("Final test failed.", style="red")
+ exit(1)