Compare commits

..

No commits in common. "master" and "v0.0.2" have entirely different histories.

25 changed files with 310 additions and 1871 deletions

4
.gitignore vendored
View file

@ -1,6 +1,5 @@
.DS_Store
node_modules
.npm
/dist
# local env files
@ -21,6 +20,3 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
# Build files
/kollagen

View file

@ -1,34 +0,0 @@
stages:
- lint
- build
deno_lint:
stage: lint
image: denoland/deno:latest
script:
- cd cli
- deno lint
deno_build:
stage: build
image: denoland/deno:latest
script:
- cd cli
- deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
artifacts:
paths:
- kollagen
app_build:
stage: build
image: node:lts
script:
- node --version && npm --version
- make app
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm
artifacts:
paths:
- dist

View file

@ -1,49 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.0] - 2020-07-20
### Added
- Specific modes can now be excluded from rotation in shuffle & recursive modes
## [0.1.0] - 2020-07-17
### Added
- Recursive collage placement mode!
### Changed
- Refactored modes - they are no longer solely responsible for drawing, but can use common rendering functions through composition.
### Fixed
- Incorrect size calculation for "Irregular column" mode.
## [0.0.4] - 2020-07-17
### Fixed
- Blending mode now properly resets after using the "Blend" collage mode.
## [0.0.3] - 2020-07-17
### Added
- "Blend" collage mode - based on a random selection of compositing operations.
- LICENSE file.
## [0.0.2] - 2020-07-16
### Changed
- Split "Grid" mode into "Clean grid" and "Irregular grid" (because "clean cropping" config only really applied to this mode.)
## [0.0.1] - 2020-07-15
### Added
- First public version!
- Basic modes: Grid, (ir)regular row, (ir)regular column, concentric modes.
[Unreleased]: https://gitlab.com/tmladek/kollagen/-/compare/v0.2.0...master
[0.2.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.1.0...v0.2.0
[0.1.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.4...v0.1.0
[0.0.4]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.3...v0.0.4
[0.0.3]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.2...v0.0.3
[0.0.2]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.1...v0.0.2
[0.0.1]: https://gitlab.com/tmladek/kollagen/-/tags/v0.0.1

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Tomáš Mládek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
all: clean deno_lint deno_docker tgbot app
deno_lint:
cd cli && deno lint
deno: ./kollagen
./kollagen:
cd cli && deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
deno_docker:
docker run --rm -v $$PWD:/app denoland/deno compile --import-map /app/cli/import_map.json --allow-read --allow-write --unstable -o /app/kollagen /app/cli/main.ts
app:
npm ci --cache .npm --prefer-offline && npm run build
tgbot: ./kollagen
docker build -t kollagen-bot -f tgbot/Dockerfile .
clean:
rm -rvf dist kollagen

View file

@ -1,96 +0,0 @@
import { CollageModes } from "../src/common/collages.ts";
import {
init,
CanvasRenderingContext2D,
EmulatedCanvas2D,
Image,
} from "https://deno.land/x/canvas@v1.3.0/mod.ts";
import { CollageImage } from "../src/common/types.ts";
const canvasKit = await init();
export class ProxyImage implements CollageImage {
private filepath: string | null;
private _image: Image | undefined;
constructor(input: string | Image) {
if (typeof input === "string") {
this.filepath = input;
} else {
this.filepath = null;
this._image = input;
}
}
public get path(): string | null {
return this.filepath;
}
public get image(): Image {
if (!this._image) {
const image = canvasKit.MakeImageFromEncoded(
Deno.readFileSync(this.filepath!)
);
if (!image) {
throw Error(`Failed loading ${this.filepath}!`);
}
this._image = image;
}
return this._image;
}
public get width(): number {
return this.image.width();
}
public get height(): number {
return this.image.height();
}
}
declare module "https://deno.land/x/canvas@v1.3.0/mod.ts" {
interface HTMLCanvasElement {
width: number;
height: number;
getContext(x: "2d"): CanvasRenderingContext2D;
}
}
export class DenoCollageModes extends CollageModes<
CanvasRenderingContext2D,
ProxyImage,
EmulatedCanvas2D
> {
createCanvas(w: number, h: number): EmulatedCanvas2D {
const canvas = canvasKit.MakeCanvas(Math.round(w), Math.round(h));
if (!canvas) {
throw Error("Error initializing canvas.");
}
return canvas;
}
canvasToImage(canvas: EmulatedCanvas2D): PromiseLike<ProxyImage> {
const image = canvasKit.MakeImageFromEncoded(canvas.toBuffer());
if (!image) {
throw Error("Something went wrong converting canvas to image.");
}
return Promise.resolve(new ProxyImage(image));
}
drawImage(
ctx: CanvasRenderingContext2D,
image: ProxyImage,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void {
ctx.drawImage(image.image, sx, sy, sw, sh, dx, dy, dw, dh);
}
}
export const denoCollageModes = new DenoCollageModes();

View file

@ -1,6 +0,0 @@
{
"imports": {
"@/common/types": "../src/common/types.ts",
"@/common/utils": "../src/common/utils.ts"
}
}

View file

@ -1,105 +0,0 @@
import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts";
import { createCanvas } from "https://deno.land/x/canvas@v1.3.0/mod.ts";
import {
collageModeType,
DisplayCollageModeType,
displayCollageModeType,
} from "../src/common/collages.ts";
import { denoCollageModes, ProxyImage } from "./collages.ts";
import { CollageConfig } from "../src/common/types.ts";
import { choice, shuffle } from "../src/common/utils.ts";
import { walkSync } from "https://deno.land/std@0.107.0/fs/mod.ts";
import { parseCollageModes, parseDisplayCollageModes } from "./util.ts";
const args = parse(Deno.args, {
alias: {
w: "width",
h: "height",
o: "output",
m: "mode",
},
boolean: ["rr"],
default: {
w: 640,
h: 640,
include: "*.png, *.jpg",
output: "collage.png",
rl: 2,
rr: false,
},
});
if (args["mode"] === true) {
console.log(displayCollageModeType.join(", "));
Deno.exit(0);
}
const files: Set<string> = new Set();
const includeExtensions = Array.from(
String(args["include"]).matchAll(/\*\.([\w]+)/g)
).map(([_, group]) => group);
args["_"].forEach((arg) => {
arg = arg.toString();
if (Deno.statSync(arg).isDirectory) {
Array.from(
walkSync(arg, {
maxDepth: Infinity,
includeDirs: false,
includeFiles: true,
exts: includeExtensions.length ? includeExtensions : undefined,
})
).forEach((entry) => files.add(entry.path));
} else {
files.add(arg);
}
});
if (files.size < 2) {
console.error("kollagen needs at least 2 images to work.");
Deno.exit(1);
}
const shuffledFiles = shuffle(Array.from(files));
const images: ProxyImage[] = shuffledFiles.map((file) => new ProxyImage(file));
const allModeKeys = args["mode"]
? parseDisplayCollageModes(args["mode"])
: displayCollageModeType;
const canvas = createCanvas(args["width"], args["height"]);
const context = canvas.getContext("2d");
const collageConfig: CollageConfig = {
numImages: args["n"],
};
const modeKey: DisplayCollageModeType = choice(allModeKeys);
if (modeKey === "recursive") {
console.log(
`Creating a recursive collage, choosing from ${shuffledFiles.length} files...`
);
await denoCollageModes.recursiveDraw(context, images, {
modes: args["rm"] ? parseCollageModes(args["rm"]) : collageModeType,
repeat: args["rr"],
level: args["rl"],
});
} else {
const mode = denoCollageModes.modes[modeKey];
console.log(
`Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`
);
const segments = mode.getSegments(context, collageConfig, images);
mode.place(context, images, segments);
console.log(
`Used: ${images
.slice(0, segments.length)
.map((img) => img.path)
.join(", ")}`
);
}
const output = args["output"];
console.log(`Saving to "${output}"...`);
await Deno.writeFile(output, canvas.toBuffer());

View file

@ -1,29 +0,0 @@
import { CollageModeType, DisplayCollageModeType, isCollageModeType, isDisplayCollageModeType } from "../src/common/collages.ts";
export function parseDisplayCollageModes(input: string): DisplayCollageModeType[] {
const result: DisplayCollageModeType[] = [];
input.split(",").map((m) => m.trim()).forEach((m) => {
if (isDisplayCollageModeType(m)) {
result.push(m);
} else {
throw Error(`"${m}" is not a valid collage mode.`);
}
});
return result;
}
export function parseCollageModes(input: string): CollageModeType[] {
const result: CollageModeType[] = [];
input.split(",").map((m) => m.trim()).forEach((m) => {
if (isCollageModeType(m)) {
result.push(m);
} else {
throw Error(`"${m}" is not a valid collage mode.`);
}
});
return result;
}

8
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "kollagen",
"version": "0.2.0",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2810,9 +2810,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001257",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz",
"integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==",
"version": "1.0.30001085",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001085.tgz",
"integrity": "sha512-x0YRFRE0pmOD90z+9Xk7jwO58p4feVNXP+U8kWV+Uo/HADyrgESlepzIkUqPgaXkpyceZU6siM1gsK7sHgplqA==",
"dev": true
},
"case-sensitive-paths-webpack-plugin": {

View file

@ -1,6 +1,6 @@
{
"name": "kollagen",
"version": "0.2.0",
"version": "0.0.2",
"homepage": "https://gitlab.com/tmladek/kollagen",
"private": true,
"scripts": {

View file

@ -1,14 +1,173 @@
import { CollageModes } from "./common/collages";
import {CollageMode} from "@/types";
import {randint, shuffle} from "@/utils";
export default class BrowserCollageModes extends CollageModes<CanvasRenderingContext2D, ImageBitmap, any> {
drawImage(ctx: CanvasRenderingContext2D, image: ImageBitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void {
ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
const collageModeType = ["clean_grid", "chaos_grid", "row", "irow", "col", "icol", "concentric_factor", "concentric_spaced"] as const;
export type CollageModeType = typeof collageModeType[number];
function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap,
x: number, y: number, w: number, h: number) {
const scaleRatio = Math.max(w / image.width, h / image.height);
ctx.drawImage(
image,
image.width / 2 - w / scaleRatio / 2, image.height / 2 - h / scaleRatio / 2,
w / scaleRatio, h / scaleRatio,
x - w / 2, y - h / 2,
w, h
);
}
createCanvas(w: number, h: number) {
throw new Error("Method not implemented.");
function getGridPoints(ctx: CanvasRenderingContext2D, idx: number) {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .25;
break;
case 1:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .25;
break;
case 2:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .75;
break;
case 3:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .75;
break;
}
canvasToImage(canvas: any): PromiseLike<ImageBitmap> {
throw new Error("Method not implemented.");
return [x, y];
}
const modes: { [key in CollageModeType]: CollageMode } = {
"clean_grid": {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
place: (ctx, images, config) => {
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const selectedImages = shuffle(images).slice(0, 4);
selectedImages.forEach((image, idx) => {
const [x, y] = getGridPoints(ctx, idx);
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"chaos_grid": {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
place: (ctx, images, config) => {
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const selectedImages = shuffle(images).slice(0, 4);
shuffle(selectedImages.map((image, idx) => [image, idx] as [ImageBitmap, number]))
.forEach(([image, idx]) => {
const [x, y] = getGridPoints(ctx, idx);
const scaleRatio = Math.max(quadrantSize[0] / image.width, quadrantSize[1] / image.height);
ctx.drawImage(image,
x - (image.width * scaleRatio / 2), y - (image.height * scaleRatio / 2),
image.width * scaleRatio, image.height * scaleRatio);
});
}
},
"row": {
name: "Regular Row",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
selectedImages.forEach((image, idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
const y = quadrantSize[1] / 2;
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"irow": {
name: "Irregular Row",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
selectedImages.forEach((image, idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
const y = quadrantSize[1] / 2;
const w = Math.min(ctx.canvas.width / 2,
quadrantSize[1] + Math.random() * (quadrantSize[1] - (quadrantSize[1] / image.height) * image.width));
cleanDraw(ctx, image, x, y, w, quadrantSize[1]);
});
}
},
"col": {
name: "Regular Column",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
selectedImages.forEach((image, idx) => {
const x = quadrantSize[0] / 2;
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"icol": {
name: "Irregular Column",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
selectedImages.forEach((image, idx) => {
const x = quadrantSize[0] / 2;
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
const h = Math.min(ctx.canvas.height / 2,
quadrantSize[0] + Math.random() * (quadrantSize[0] - (quadrantSize[0] / image.width) * image.height));
cleanDraw(ctx, image, x, y, quadrantSize[0], h);
});
},
},
"concentric_factor": {
name: "Constant factor concentric",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const x = ctx.canvas.width / 2;
const y = ctx.canvas.height / 2;
let factor: number;
if (Math.random() > .5) {
const factors = [1 / Math.sqrt(2), .5, .88];
factor = factors[Math.floor(Math.random() * factors.length)];
} else {
factor = 1 - (1 / selectedImages.length);
}
selectedImages.forEach((image, idx) => {
const ratio = Math.pow(factor, idx);
cleanDraw(ctx, image, x, y, ctx.canvas.width * ratio, ctx.canvas.height * ratio);
});
}
},
"concentric_spaced": {
name: "Equally spaced concentric",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const x = ctx.canvas.width / 2;
const y = ctx.canvas.height / 2;
selectedImages.forEach((image, idx) => {
cleanDraw(
ctx, image, x, y,
ctx.canvas.width - (ctx.canvas.width / selectedImages.length * idx),
ctx.canvas.height - (ctx.canvas.height / selectedImages.length * idx),
);
});
}
}
};
export default modes;

View file

@ -1,397 +0,0 @@
import {
CollageCanvas,
CollageConfig,
CollageContext,
CollageImage,
CollageMode,
Segment,
} from "@/common/types";
import { choice, randint, range, shuffle } from "@/common/utils";
export const collageModeType = [
"clean_grid",
"chaos_grid",
"row",
"irow",
"col",
"icol",
"concentric_factor",
"concentric_spaced",
"blend",
] as const;
export type CollageModeType = typeof collageModeType[number];
export function isCollageModeType(value: string): value is CollageModeType {
return (collageModeType as readonly string[]).includes(value);
}
export const displayCollageModeType = [
...collageModeType,
"recursive",
] as const;
export type DisplayCollageModeType = typeof displayCollageModeType[number];
export function isDisplayCollageModeType(
value: string
): value is DisplayCollageModeType {
return (displayCollageModeType as readonly string[]).includes(value);
}
export interface RecursiveCollageConfig {
level: number;
repeat: boolean;
modes: readonly CollageModeType[];
}
export abstract class CollageModes<
C extends CollageContext,
I extends CollageImage,
CS extends CollageCanvas
> {
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
clean_grid: {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4,
},
getSegments: this.getGridSegments,
place: this.cleanPlace.bind(this),
},
chaos_grid: {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4,
},
getSegments: this.getGridSegments,
place: (ctx, images, segments) => {
const shuffledImages = shuffle(images);
shuffle(
segments.map((segment, idx) => [segment, idx] as [Segment, number])
).forEach(([segment, idx]) => {
const image = shuffledImages[idx];
const scaleRatio = Math.max(
segment.w / image.width,
segment.h / image.height
);
this.drawImage(
ctx,
image,
0,
0,
image.width,
image.height,
segment.x - (image.width * scaleRatio) / 2,
segment.y - (image.height * scaleRatio) / 2,
image.width * scaleRatio,
image.height * scaleRatio
);
});
},
},
row: {
name: "Regular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
irow: {
name: "Irregular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
const irregularWidth = images
? segmentSize[0] +
Math.random() *
((segmentSize[1] / images[idx].height) * images[idx].width -
segmentSize[0])
: segmentSize[0] + Math.random() * segmentSize[0] * 0.5;
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: Math.min(ctx.canvas.width / 2, irregularWidth),
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
col: {
name: "Regular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
icol: {
name: "Irregular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
const irregularHeight = images
? segmentSize[1] +
Math.random() *
((segmentSize[0] / images[idx].width) * images[idx].height -
segmentSize[1])
: segmentSize[1] + Math.random() * segmentSize[1] * 0.5;
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: Math.min(ctx.canvas.height / 2, irregularHeight),
};
});
},
place: this.cleanPlace.bind(this),
},
concentric_factor: {
name: "Constant factor concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
let factor: number;
if (Math.random() > 0.5) {
factor = choice([1 / Math.sqrt(2), 0.5, 0.88]);
} else {
factor = 1 - 1 / numImages;
}
return range(numImages).map((idx) => {
const ratio = Math.pow(factor, idx);
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width * ratio,
h: ctx.canvas.height * ratio,
};
});
},
place: this.cleanPlace.bind(this),
},
concentric_spaced: {
name: "Equally spaced concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((idx) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width - (ctx.canvas.width / numImages) * idx,
h: ctx.canvas.height - (ctx.canvas.height / numImages) * idx,
};
});
},
place: this.cleanPlace.bind(this),
},
blend: {
name: "Blending",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((_) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width,
h: ctx.canvas.height,
};
});
},
place: (ctx, images, segments) => {
ctx.globalCompositeOperation = choice([
"difference",
"saturation",
"soft-light",
"overlay",
]);
this.cleanPlace(ctx, images, segments);
},
},
};
public async recursiveDraw(
context: C,
images: I[],
recursiveConfig: RecursiveCollageConfig
) {
const localImages = images.concat();
const rootSegment: Segment = {
x: 0,
y: 0,
w: context.canvas.width,
h: context.canvas.height,
};
const processSegment = async (
segment: Segment,
level: number
): Promise<I> => {
// console.debug(segment, level);
if (segment === rootSegment || level <= recursiveConfig.level - 1) {
const canvas = this.createCanvas(segment.w, segment.h);
const modeKey = choice(recursiveConfig.modes);
// console.debug(modeKey);
const mode = this.modes[modeKey];
const ctx = canvas.getContext("2d") as C;
const segments = mode.getSegments(ctx);
// console.debug(segments);
const bitmaps = await Promise.all(
segments.map((segment: Segment) => processSegment(segment, level + 1))
);
mode.place(ctx, bitmaps, segments);
return await this.canvasToImage(canvas);
} else {
if (recursiveConfig.repeat) {
return choice(localImages);
} else {
if (localImages.length > 0) {
return localImages.pop() as I;
} else {
throw "RAN OUT OF IMAGES";
}
}
}
};
const result = await processSegment(rootSegment, 0);
this.drawImage(
context,
result,
0,
0,
result.width,
result.height,
0,
0,
context.canvas.width,
context.canvas.height
);
}
private cleanDraw(
ctx: C,
image: I,
x: number,
y: number,
w: number,
h: number
) {
const scaleRatio = Math.max(w / image.width, h / image.height);
this.drawImage(
ctx,
image,
image.width / 2 - w / scaleRatio / 2,
image.height / 2 - h / scaleRatio / 2,
w / scaleRatio,
h / scaleRatio,
x - w / 2,
y - h / 2,
w,
h
);
}
private cleanPlace(ctx: C, images: I[], segments: Segment[]) {
segments.forEach((segment, idx) => {
this.cleanDraw(
ctx,
images[idx],
segment.x,
segment.y,
segment.w,
segment.h
);
});
}
private getGridSegments(ctx: C, config?: CollageConfig) {
return [0, 1, 2, 3].map((idx) => {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.25;
break;
case 1:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.25;
break;
case 2:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.75;
break;
case 3:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.75;
break;
}
return {
x,
y,
w: ctx.canvas.width / 2,
h: ctx.canvas.height / 2,
};
});
}
abstract createCanvas(w: number, h: number): CS;
abstract canvasToImage(canvas: CS): PromiseLike<I>;
abstract drawImage(
ctx: C,
image: I,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void;
}

View file

@ -1,35 +0,0 @@
export interface CollageMode<C extends CollageContext, I extends CollageImage> {
name: string;
minImages: number;
getSegments: (ctx: C, config?: CollageConfig, images?: I[]) => Segment[];
place: (ctx: C, images: I[], segments: Segment[]) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}
export interface Segment {
x: number;
y: number;
w: number;
h: number;
}
export interface CollageContext {
globalCompositeOperation: string;
canvas: CollageCanvas
}
export interface CollageCanvas {
width: number;
height: number;
getContext: (x: '2d') => CollageContext | null
}
export interface CollageImage {
width: number;
height: number;
}

View file

@ -1,117 +1,38 @@
<template>
<div class="collage">
<div class="canvas">
<canvas
id="canvas"
ref="canvas"
:width="canvasSize.width"
:height="canvasSize.height"
></canvas>
<canvas id="canvas" ref="canvas" :width="canvasSize.width" :height="canvasSize.height"></canvas>
<div class="canvas-size">
<label>
Width:
<input type="number" step="16" min="128" v-model="canvasSize.width" />
<input type="number" step="16" min="128" v-model="canvasSize.width">
</label>
<label>
Height:
<input
type="number"
step="16"
min="128"
v-model="canvasSize.height"
/>
<input type="number" step="16" min="128" v-model="canvasSize.height">
</label>
</div>
</div>
<div class="controls">
<div class="modes">
<ul class="modes-list">
<li
v-for="(mode, idx) in modes.modes"
:class="[
'mode',
{
disabled: images.length < mode.minImages,
excluded: excludedModes.includes(idx),
<label v-for="(mode, idx) in modes"
:class="{disabled: images.length < mode.minImages,
selected: idx === currentModeType,
lastActive: lastActiveModeTypes.includes(idx),
},
]"
:key="idx"
>
<label class="handle"
>{{ mode.name
}}<input type="radio" :value="idx" v-model="currentModeType"
/></label>
<span
class="mode-plus"
v-if="
['recursive', 'shuffle'].includes(currentModeType) &&
images.length >= minImages
"
>
<span class="separator"></span>
<label
>{{ excludedModes.includes(idx) ? "o" : "x"
}}<input type="checkbox" :value="idx" v-model="excludedModes"
/></label>
</span>
</li>
</ul>
<hr />
<label
:class="{
disabled: images.length < minImages,
selected: 'shuffle' === currentModeType,
}"
>
!SHUFFLE ALL!
<input type="radio" value="shuffle" v-model="currentModeType" />
</label>
<label
:class="{
disabled: images.length < minImages,
selected: 'recursive' === currentModeType,
}"
>
#RECURSIVE#
<input type="radio" value="recursive" v-model="currentModeType" />
lastActive: idx === lastActiveModeType}">
{{mode.name}}
<input type="radio" :value="idx" v-model="currentModeType">
</label>
</div>
<button :disabled="images.length < minImages" @click="renderCollage">
REPAINT
</button>
<hr />
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button>
<hr>
<div class="config">
<template v-if="currentModeType !== 'recursive'">
<label class="config-numimages">
#N of images:
<input
type="number"
:min="minImages"
:max="images.length"
<input type="number" :min="currentMode.minImages" :max="images.length"
placeholder="RND"
:disabled="Object.keys(forceConfig).includes('numImages')"
:value="forceConfig.numImages || collageConfig.numImages"
@input="(n) => (collageConfig.numImages = n)"
/>
v-model="forceConfig.numImages || collageConfig.numImages">
</label>
</template>
<template v-else>
<label>
Recursion levels:
<input
type="number"
:min="1"
:max="10"
v-model="recursiveConfig.level"
/>
</label>
<label>
<input type="checkbox" v-model="recursiveConfig.repeat" />
Repeat images?
</label>
</template>
</div>
</div>
</div>
@ -119,12 +40,10 @@
<script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
import BrowserCollageModes from "../collages";
import { CollageModeType } from "../common/collages";
import { CollageConfig, CollageMode, Segment } from "../common/types";
import { choice, shuffle } from "../common/utils";
import collageModes, {CollageModeType} from "../collages";
import {CollageConfig, CollageMode} from "@/types";
type DisplayCollageModeType = CollageModeType | "shuffle" | "recursive";
type DisplayCollageModeType = CollageModeType | & "shuffle";
@Component
export default class Collage extends Vue {
@ -132,135 +51,59 @@ export default class Collage extends Vue {
private context!: CanvasRenderingContext2D;
private canvasSize = {
width: 640,
height: 640,
height: 640
};
private collageConfig: CollageConfig = {
numImages: undefined,
};
private recursiveConfig = {
level: 2,
repeat: true,
numImages: undefined
};
private currentModeType: DisplayCollageModeType = "shuffle";
private lastActiveModeTypes: CollageModeType[] = [];
private excludedModes: CollageModeType[] = [];
private modes = new BrowserCollageModes();
private get minImages() {
if (
this.currentModeType === "shuffle" ||
this.currentModeType === "recursive"
) {
return Math.min(
...Object.values(this.modes.modes).map((mode) => mode.minImages)
);
} else {
return this.modes.modes[this.currentModeType].minImages;
private lastActiveModeType: DisplayCollageModeType | null = null;
private modes: { [key in DisplayCollageModeType]: CollageMode } = {
...collageModes,
"shuffle": {
name: "Shuffle all!",
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)),
place: (ctx, images, config) => {
const permissibleModeKeys = Object.keys(collageModes)
.filter(k => collageModes[k as CollageModeType].minImages <= images.length) as CollageModeType[];
const randomModeType = permissibleModeKeys[Math.floor(Math.random() * permissibleModeKeys.length)];
const randomMode = collageModes[randomModeType];
this.setLastActiveModeType(randomModeType);
randomMode.place(ctx, images, config);
}
}
};
// wtf vue?
private setLastActiveModeType(lastActiveModeType: any) {
this.lastActiveModeType = lastActiveModeType;
}
private get currentMode() {
return this.modes[this.currentModeType];
}
private get lastMode() {
if (this.lastActiveModeTypes.length === 1) {
return this.modes.modes[this.lastActiveModeTypes[0]];
}
return this.lastActiveModeType ? this.modes[this.lastActiveModeType] : undefined;
}
private get forceConfig() {
return this.lastMode ? this.lastMode.forceConfig || {} : {};
}
private mounted() {
const canvas = this.$refs.canvas as HTMLCanvasElement;
const canvas = (this.$refs.canvas as HTMLCanvasElement);
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
}
@Watch("images")
@Watch("currentModeType")
@Watch("currentMode")
@Watch("collageConfig", {deep: true})
@Watch("recursiveConfig", { deep: true })
private renderCollage() {
if (this.images.length >= this.minImages) {
this.reset();
const permissibleModeKeys = (
Object.keys(this.modes.modes) as CollageModeType[]
).filter(
(k) =>
!this.excludedModes.includes(k) &&
this.modes.modes[k].minImages <= this.images.length
);
if (this.currentModeType !== "recursive") {
let mode: CollageMode<any, any>;
if (this.currentModeType === "shuffle") {
const randomModeType = choice(permissibleModeKeys);
this.lastActiveModeTypes = [randomModeType];
mode = this.modes.modes[randomModeType];
} else {
this.lastActiveModeTypes = [this.currentModeType];
mode = this.modes.modes[this.currentModeType];
}
const shuffledImages = shuffle(this.images);
const segments = mode.getSegments(
this.context,
this.collageConfig,
shuffledImages
);
mode.place(this.context, shuffledImages, segments);
} else {
this.lastActiveModeTypes = [];
const shuffledImages = shuffle(this.images);
const rootSegment: Segment = {
x: 0,
y: 0,
w: this.context.canvas.width,
h: this.context.canvas.height,
};
const processSegment = async (
segment: Segment,
level: number
): Promise<ImageBitmap> => {
console.debug(segment, level);
if (
segment === rootSegment ||
level <= this.recursiveConfig.level - 1
) {
let canvas = document.createElement("canvas");
canvas.width = segment.w;
canvas.height = segment.h;
let modeKey = choice(permissibleModeKeys);
console.debug(modeKey);
this.lastActiveModeTypes.push(modeKey);
let mode = this.modes.modes[modeKey];
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
let segments = mode.getSegments(ctx);
console.debug(segments);
let bitmaps = await Promise.all(
segments.map((segment) => processSegment(segment, level + 1))
);
mode.place(ctx, bitmaps, segments);
return await createImageBitmap(canvas);
} else {
if (this.recursiveConfig.repeat) {
return choice(shuffledImages);
} else {
if (shuffledImages.length > 0) {
return shuffledImages.pop() as ImageBitmap;
} else {
throw "RAN OUT OF IMAGES";
}
}
}
};
processSegment(rootSegment, 0)
.then((finalCollage) => {
console.debug(finalCollage);
this.context.drawImage(finalCollage, 0, 0);
})
.catch((error) => {
alert(error);
});
}
if (this.images.length >= this.currentMode.minImages) {
this.lastActiveModeType = this.currentModeType;
this.currentMode.place(this.context, this.images, this.collageConfig);
}
}
@ -270,17 +113,10 @@ export default class Collage extends Vue {
this.renderCollage();
});
}
private reset() {
this.context.globalCompositeOperation = "source-over";
const canvas = this.$refs.canvas as HTMLCanvasElement;
this.context.clearRect(0, 0, canvas.width, canvas.height);
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<!--suppress CssUnusedSymbol -->
<style scoped>
.collage {
margin: 2rem;
@ -313,55 +149,17 @@ export default class Collage extends Vue {
align-items: center;
}
.modes-list {
list-style: none;
padding: 0;
}
.mode {
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
.separator {
border: 1px solid gray;
width: 0;
margin: 0 0.5em;
align-self: stretch;
}
.mode .mode-plus,
.mode .mode-plus label {
color: gray;
font-size: 14px;
}
.mode-plus {
display: flex;
align-items: center;
}
.controls .modes hr {
margin-top: 0.5rem;
width: 100%;
color: lightgray;
}
.controls label {
font-size: 14pt;
cursor: pointer;
margin: 0.25rem;
margin: .25rem;
}
.modes input {
display: none;
}
.controls button,
.controls hr,
.controls .config {
.controls button, .controls hr, .controls .config {
margin-top: 1rem;
}
@ -382,16 +180,11 @@ export default class Collage extends Vue {
pointer-events: none;
}
.excluded .handle {
color: gray;
text-decoration: line-through;
}
.selected {
font-weight: bold;
}
.lastActive .handle {
.lastActive {
text-decoration: underline;
}
</style>

10
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
export interface CollageMode {
name: string;
minImages: number;
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], config: CollageConfig) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}

View file

@ -17,11 +17,3 @@ export function shuffle<T>(a: T[]): T[] {
export function randint(n: number) {
return Math.floor(Math.random() * n);
}
export function choice<T>(arr: readonly T[]): T {
return arr[randint(arr.length)];
}
export function range(n: number): number[] {
return Array.from({length: n}, (x, i) => i);
}

View file

@ -1,22 +0,0 @@
FROM python:3-slim
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
RUN pip install poetry
WORKDIR /app
COPY tgbot/poetry.lock tgbot/pyproject.toml /app/
RUN poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction --no-ansi
COPY /tgbot/ /app/
COPY kollagen /usr/bin/kollagen
CMD python kollagen-bot/main.py

View file

Binary file not shown.

View file

@ -1 +0,0 @@
__version__ = '0.1.0'

View file

@ -1,270 +0,0 @@
import logging
import os
import subprocess
from typing import List, Optional
from tempfile import NamedTemporaryFile
from telegram import Update
from telegram.ext import (
Updater,
CommandHandler,
DictPersistence,
CallbackContext,
)
from telegram.parsemode import ParseMode
from parser import SafeArgumentParser, safe_str
class KollagenBot:
def __init__(
self, tg_token: str, kollagen_path: str, base_dir: Optional[str]
) -> None:
self.logger = logging.getLogger("kollagen")
self.kollagen_path = kollagen_path
self.base_dir = os.path.abspath(base_dir) if base_dir else None
self._init_parser()
self.updater = Updater(
tg_token, persistence=DictPersistence(user_data_json="{}")
)
dispatcher = self.updater.dispatcher
dispatcher.add_handler(CommandHandler("start", self.tg_start))
dispatcher.add_handler(CommandHandler("help", self.tg_help))
dispatcher.add_handler(CommandHandler("generate", self.tg_generate))
dispatcher.add_handler(CommandHandler("g", self.tg_generate))
dispatcher.add_handler(CommandHandler("regenerate", self.tg_regenerate))
dispatcher.add_handler(CommandHandler("r", self.tg_regenerate))
dispatcher.add_error_handler(self.tg_error)
def _init_parser(self):
parser = SafeArgumentParser(prog="/generate", add_help=False)
parser.add_argument(
"directories",
metavar="path",
type=safe_str,
nargs="*",
default=[self.base_dir] if self.base_dir else [],
help="Directories or files to process. By default, the entire base directory is processed.",
)
parser.add_argument(
"-m",
dest="mode",
metavar="mode",
type=safe_str,
nargs="?",
const=True,
help=f"Collage modes to use. By default, one is chosen at random. Multiple modes can be specified, separated by commas. When no value is specified, all modes are listed.",
)
parser.add_argument(
"-n",
dest="num_images",
metavar="N",
type=int,
help=f"How many images to use in a single collage. Random (or collage-dependant) by default.",
)
parser.add_argument(
"-w",
dest="width",
type=int,
default=640,
help=f"Width of resulting output (in px). 640px by default.",
)
parser.add_argument(
"-h",
dest="height",
type=int,
default=640,
help=f"Height of resulting output (in px). 640px by default.",
)
parser.add_argument(
"--rm",
dest="recursive_modes",
type=safe_str,
help=f"Collage modes (comma-separated) to use in a recursive collage. All by default.",
)
parser.add_argument(
"--rl",
dest="recursive_level",
type=int,
default=2,
help=f"Level/depth of recursive collage. 2 by default.",
)
parser.add_argument(
"--rr",
dest="recursive_repeat",
action="store_true",
help=f"Allow repeating images in (different levels of) recursive collages. False by default.",
)
self.parser = parser
def _get_modes(self):
modes = subprocess.run(
[self.kollagen_path, "-m"], check=True, capture_output=True
)
return modes.stdout.decode("utf-8").strip().split(", ")
def tg_start(self, update: Update, context: CallbackContext):
update.message.reply_text("Hi! I make random collages. Check out https://gitlab.com/tmladek/kollagen and /help. Here's one to get you started:")
self._process([], update)
def tg_generate(self, update: Update, context: CallbackContext):
cmd_line = update.message.text.split(" ")[1:]
success = self._process(cmd_line, update)
if success and context.user_data is not None:
context.user_data["last_cmd_line"] = cmd_line
def tg_regenerate(self, update: Update, context: CallbackContext):
if context.user_data and context.user_data.get("last_cmd_line"):
self._process(context.user_data["last_cmd_line"], update)
else:
update.message.reply_text("No previous command to regenerate!")
def _process(self, cmd_line: List[str], update: Update):
self.logger.info(
f"Generating from {update.effective_user}, with cmd_line: `{cmd_line}`"
)
args = self.parser.parse_args(cmd_line)
if args.mode is True:
update.message.reply_text(
f"Available modes: {', '.join(self._get_modes())}"
)
return False
directories = []
for dir in args.directories:
for possible in [
os.path.join(self.base_dir or "./", dir),
os.path.join(self.base_dir or "./", dir.upper()),
]:
if os.path.exists(possible):
directories.append(possible)
break
else:
raise FileNotFoundError(f'"{dir}" does not exist.')
mode = ["-m", args.mode] if args.mode else []
num_images = ["-n", str(args.num_images)] if args.num_images else []
recursive_level = (
["--rl", str(args.recursive_level)] if args.recursive_level else []
)
recursive_repeat = ["--rr"] if args.recursive_repeat else []
recursive_modes = (
["--rm", str(args.recursive_modes)] if args.recursive_modes else []
)
with NamedTemporaryFile(suffix=".png") as ntf:
shell_cmd_line = [
self.kollagen_path,
*directories,
"-w",
str(args.width),
"-h",
str(args.height),
*mode,
*num_images,
*recursive_level,
*recursive_repeat,
*recursive_modes,
"-o",
ntf.name,
]
self.logger.debug(f"Running: " + str(shell_cmd_line))
result = subprocess.run(
shell_cmd_line,
check=True,
capture_output=True,
timeout=60,
env={
'NO_COLOR': "1"
}
)
ntf.seek(0)
used_line = next(
(
line
for line in result.stdout.decode("utf-8").splitlines()
if line.startswith("Used: ")
),
"",
).replace(f"{self.base_dir}/" if self.base_dir else "", "")
caption = ""
caption += (
f"`{' '.join(['/generate', *cmd_line])}`\n" if len(cmd_line) else ""
)
caption += used_line.replace("_", "\\_")
caption = caption[:200]
update.message.reply_photo(
ntf,
caption=caption,
parse_mode=ParseMode.MARKDOWN,
)
return True
def tg_help(self, update: Update, context: CallbackContext):
update.message.reply_text(
f"```{self.parser.format_help()}```", parse_mode=ParseMode.MARKDOWN
)
def tg_error(self, update: object, context: CallbackContext) -> None:
self.logger.error(
msg="Exception while handling an update:", exc_info=context.error
)
if isinstance(update, Update):
if isinstance(context.error, subprocess.CalledProcessError):
error_display = context.error.stderr.decode('utf-8')
else:
error_display = str(context.error)
error_display = error_display[:2500]
update.message.reply_text(
f"Something is fucked!\n{error_display}"
)
def start_idle(self):
self.updater.start_polling()
self.updater.idle()
def main() -> None:
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=getattr(logging, os.getenv("LOG_LEVEL", "info").upper()),
)
tg_token = os.getenv("TG_TOKEN")
if not tg_token:
logging.error("TG_TOKEN is required.")
exit(1)
if env_kollagen_path := os.getenv("KOLLAGEN_PATH"):
if os.path.exists(env_kollagen_path):
kollagen_path = env_kollagen_path
else:
logging.error(f"kollagen not found! {env_kollagen_path} does not exist.")
exit(1)
else:
which = subprocess.run(["which", "kollagen"], capture_output=True)
try:
which.check_returncode()
kollagen_path = which.stdout.decode("utf-8").strip()
except subprocess.CalledProcessError:
logging.error(
"kollagen not found! KOLLAGEN_PATH not specified and `kollagen` isn't in $PATH."
)
exit(1)
bot = KollagenBot(tg_token, kollagen_path, os.getenv("BASE_DIR"))
bot.start_idle()
if __name__ == "__main__":
main()

View file

@ -1,16 +0,0 @@
import argparse
import re
class ArgumentParserError(RuntimeError):
pass
class SafeArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise ArgumentParserError(message)
def safe_str(val: str):
if re.findall(r'[^\w,]', val):
raise RuntimeError("No special characters in arguments allowed!")
return val

393
tgbot/poetry.lock generated
View file

@ -1,393 +0,0 @@
[[package]]
name = "apscheduler"
version = "3.6.3"
description = "In-process task scheduler with Cron-like capabilities"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pytz = "*"
six = ">=1.4.0"
tzlocal = ">=1.2"
[package.extras]
asyncio = ["trollius"]
doc = ["sphinx", "sphinx-rtd-theme"]
gevent = ["gevent"]
mongodb = ["pymongo (>=2.8)"]
redis = ["redis (>=3.0)"]
rethinkdb = ["rethinkdb (>=2.4.0)"]
sqlalchemy = ["sqlalchemy (>=0.8)"]
testing = ["pytest", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"]
tornado = ["tornado (>=4.3)"]
twisted = ["twisted"]
zookeeper = ["kazoo"]
[[package]]
name = "backports.zoneinfo"
version = "0.2.1"
description = "Backport of the standard library zoneinfo module"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
tzdata = ["tzdata"]
[[package]]
name = "black"
version = "21.9b0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
[package.dependencies]
click = ">=7.1.2"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0,<1"
platformdirs = ">=2"
regex = ">=2020.1.8"
tomli = ">=0.2.6,<2.0.0"
typing-extensions = [
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
{version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
]
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
python2 = ["typed-ast (>=1.4.2)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cachetools"
version = "4.2.2"
description = "Extensible memoizing collections and decorators"
category = "main"
optional = false
python-versions = "~=3.5"
[[package]]
name = "certifi"
version = "2021.5.30"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "click"
version = "8.0.1"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pathspec"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "platformdirs"
version = "2.3.0"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "python-telegram-bot"
version = "13.7"
description = "We have made you a wrapper you can't refuse"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
APScheduler = "3.6.3"
cachetools = "4.2.2"
certifi = "*"
pytz = ">=2018.6"
tornado = ">=6.1"
[package.extras]
json = ["ujson"]
passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"]
socks = ["pysocks"]
[[package]]
name = "pytz"
version = "2021.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "regex"
version = "2021.8.28"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "1.2.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "tornado"
version = "6.1"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
category = "main"
optional = false
python-versions = ">= 3.5"
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "tzdata"
version = "2021.1"
description = "Provider of IANA time zone data"
category = "main"
optional = false
python-versions = ">=2"
[[package]]
name = "tzlocal"
version = "3.0"
description = "tzinfo object for the local timezone"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "a1390250d2dc7c6ca705c53cebcec951a6dd79717bb0f902b0177b307fac8bc3"
[metadata.files]
apscheduler = [
{file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"},
{file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"},
]
"backports.zoneinfo" = [
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
{file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
]
black = [
{file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"},
{file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"},
]
cachetools = [
{file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"},
{file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"},
]
certifi = [
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
]
click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
{file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
]
python-telegram-bot = [
{file = "python-telegram-bot-13.7.tar.gz", hash = "sha256:24df75459e335b96baffa6991679f844bd426978af5a69ca419a0ac43a40602c"},
{file = "python_telegram_bot-13.7-py3-none-any.whl", hash = "sha256:3bf210862744068aa789d5110f8e3a00d98912ce50863384836440a18abf76b5"},
]
pytz = [
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
]
regex = [
{file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
{file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
{file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
{file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
{file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
{file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
{file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
{file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
{file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
{file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
{file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
{file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
{file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
{file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
{file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
{file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
tomli = [
{file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
{file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
]
tornado = [
{file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
{file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
{file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"},
{file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"},
{file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"},
{file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"},
{file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"},
{file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"},
{file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"},
{file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"},
{file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"},
{file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"},
{file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"},
{file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"},
{file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"},
{file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"},
{file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"},
{file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"},
{file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"},
{file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"},
{file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"},
{file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"},
{file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"},
{file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"},
{file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"},
{file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"},
{file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"},
{file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"},
{file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"},
{file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"},
{file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"},
{file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"},
{file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"},
{file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"},
{file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"},
{file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"},
{file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"},
{file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"},
{file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"},
{file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
{file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
tzdata = [
{file = "tzdata-2021.1-py2.py3-none-any.whl", hash = "sha256:9ad21eada54c97001e3e9858a674b3ee6bebe4a4fb2b58465930f2af0ba6c85d"},
{file = "tzdata-2021.1.tar.gz", hash = "sha256:e19c7351f887522a1ac739d21041e592ddde6dd1b764fdefa8f7b2b3551d3d38"},
]
tzlocal = [
{file = "tzlocal-3.0-py3-none-any.whl", hash = "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559"},
{file = "tzlocal-3.0.tar.gz", hash = "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12"},
]

View file

@ -1,16 +0,0 @@
[tool.poetry]
name = "kollagen-bot"
version = "0.1.0"
description = ""
authors = ["Tomáš Mládek <t@mldk.cz>"]
[tool.poetry.dependencies]
python = "^3.8"
python-telegram-bot = "^13.7"
[tool.poetry.dev-dependencies]
black = {version = "^21.9b0", allow-prereleases = true}
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"