Compare commits

...

4 Commits

Author SHA1 Message Date
61448a2521 Python working 2021-11-02 22:12:25 -03:00
b0f7c9b2b1 Cleanup 2021-11-02 22:12:11 -03:00
0c44554375 Camelot reading 2021-11-02 15:37:36 -03:00
5ee267568a PDF reading with python 2021-11-01 11:09:26 -03:00
66 changed files with 1226 additions and 27 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

81
.idea/contabilidad.iml generated Normal file
View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/kint-php/kint" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/zeuxisoo/slim-whoops" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/philo/laravel-blade" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nesbot/carbon" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/type" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/support" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/lines-of-code" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/filesystem" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/events" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/contracts" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/simple-cache" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/code-unit" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/view" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-server-handler" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/doctrine/instantiator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/container" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-factory" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/doctrine/inflector" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-server-middleware" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/recursion-context" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/resource-operations" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/code-unit-reverse-lookup" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/filp/whoops" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/polyfill-ctype" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/debug" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nyholm/psr7-server" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/translation" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nyholm/psr7" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nikic/fast-route" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/php-di" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/phpdoc-reader" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/slim-bridge" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/invoker" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/phpunit" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpspec/prophecy" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/opis/closure" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-http/message-factory" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/rubellum/slim-blade-view" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/slim/slim" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/contabilidad.iml" filepath="$PROJECT_DIR$/.idea/contabilidad.iml" />
</modules>
</component>
</project>

87
.idea/php.xml generated Normal file
View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/ui/vendor/kint-php/kint" />
<path value="$PROJECT_DIR$/ui/vendor/zeuxisoo/slim-whoops" />
<path value="$PROJECT_DIR$/ui/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/ui/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/ui/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/ui/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/ui/vendor/philo/laravel-blade" />
<path value="$PROJECT_DIR$/ui/vendor/nesbot/carbon" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/support" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/filesystem" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/events" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/contracts" />
<path value="$PROJECT_DIR$/ui/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/view" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-server-handler" />
<path value="$PROJECT_DIR$/ui/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/container" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/ui/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/ui/vendor/psr/container" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-server-middleware" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/ui/vendor/psr/log" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/ui/vendor/composer" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/ui/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/ui/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/ui/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/ui/vendor/filp/whoops" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/debug" />
<path value="$PROJECT_DIR$/ui/vendor/nyholm/psr7-server" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/ui/vendor/nyholm/psr7" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/ui/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/ui/vendor/nikic/fast-route" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/php-di" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/phpdoc-reader" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/slim-bridge" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/invoker" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/ui/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/ui/vendor/phpspec/prophecy" />
<path value="$PROJECT_DIR$/ui/vendor/opis/closure" />
<path value="$PROJECT_DIR$/ui/vendor/php-http/message-factory" />
<path value="$PROJECT_DIR$/ui/vendor/rubellum/slim-blade-view" />
<path value="$PROJECT_DIR$/ui/vendor/slim/slim" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.0">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/ui/vendor/autoload.php" />
</phpunit_settings>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

11
TODO.md Normal file
View File

@ -0,0 +1,11 @@
# Contabilidad
1. Obtener pdf de cartolas desde email.
1. Conectar a Email por IMAP.
1. Buscar emails con cartolas.
1. Descargar cartolas.
1. Guardar de forma ordenada.
1. Extraer información e ingresar a base de datos a traves de API.
1. Abrir archivos y leer.
1. Formatear datos.
1. Mandar a API.

View File

@ -1,5 +1,9 @@
FROM php:8-fpm
RUN docker-php-ext-install pdo pdo_mysql
RUN apt-get update -y && apt-get install -y git libzip-dev zip
RUN docker-php-ext-install pdo pdo_mysql zip
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /app

View File

@ -0,0 +1,21 @@
<?php
namespace Contabilidad\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\Json;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\PdfHandler;
class Import {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$post = $request->getParsedBody();
return $this->withJson($response, $post);
}
public function uploads(Request $request, Response $response, PdfHandler $handler): Response {
$output = $handler->load();
return $this->withJson($response, $output);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Contabilidad\Common\Service;
use GuzzleHttp\Client;
class PdfHandler {
protected Client $client;
protected string $folder;
protected string $url;
public function __construct(Client $client, string $pdf_folder, string $url) {
$this->client = $client;
$this->folder = $pdf_folder;
$this->url = $url;
}
public function load(): ?array {
$folder = $this->folder;
$files = new \DirectoryIterator($folder);
$output = [];
foreach ($files as $file) {
if ($file->isDir() or $file->getExtension() != 'pdf') {
continue;
}
$output []= ['filename' => $file->getBasename()];
}
$response = $this->client->post($this->url, ['json' => ['files' => $output]]);
$output = json_decode($response->getBody());
return $output;
}
}

View File

@ -10,7 +10,11 @@
"zeuxisoo/slim-whoops": "^0.7.3",
"provm/controller": "^1.0",
"provm/models": "^1.0.0-rc3",
"nesbot/carbon": "^2.50"
"nesbot/carbon": "^2.50",
"robmorgan/phinx": "^0.12.9",
"odan/phinx-migrations-generator": "^5.4",
"martin-mikac/csv-to-phinx-seeder": "^1.6",
"guzzlehttp/guzzle": "^7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.5",

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TipoCategoria extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('tipos_categoria')
->addColumn('descripcion', 'string')
->addColumn('activo', 'boolean')
->create();
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TipoEstadoConeccion extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('tipos_estado_coneccion')
->addColumn('descripcion', 'string')
->create();
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TipoCuenta extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('tipos_cuenta')
->addColumn('descripcion', 'string')
->create();
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Categoria extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('categorias')
->addColumn('nombre', 'string')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_categoria')
->create();
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Coneccion extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('conecciones')
->addColumn('key', 'string')
->create();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Cuenta extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('cuentas')
->addColumn('nombre', 'string')
->addColumn('categoria_id', 'integer')
->addForeignKey('categoria_id', 'categorias')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_cuenta')
->create();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class EstadoConeccion extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('estados_coneccion')
->addColumn('coneccion_id', 'integer')
->addForeignKey('coneccion_id', 'conecciones')
->addColumn('fecha', 'date')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_estado_coneccion')
->create();
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Transaccion extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('transacciones')
->addColumn('debito_id', 'integer')
->addForeignKey('debito_id', 'cuentas')
->addColumn('credito_id', 'integer')
->addForeignKey('credito_id', 'cuentas')
->addColumn('fecha', 'datetime')
->addColumn('glosa', 'string')
->addColumn('detalle', 'text')
->addColumn('valor', 'double')
->create();
}
}

View File

@ -0,0 +1,36 @@
<?php
use Phinx\Seed\AbstractSeed;
class TipoCuenta extends AbstractSeed
{
/**
* Run Method.
*
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
* https://book.cakephp.org/phinx/0/en/seeding.html
*/
public function run()
{
$data = [
[
'descripcion' => 'Ganancia'
],
[
'descripcion' => 'Activo'
],
[
'descripcion' => 'Pasivo'
],
[
'descripcion' => 'Perdida'
]
];
$this->table('tipos_cuenta')
->insert($data)
->saveData();
}
}

View File

@ -0,0 +1,30 @@
<?php
use Phinx\Seed\AbstractSeed;
class TipoEstadoConeccion extends AbstractSeed
{
/**
* Run Method.
*
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
* https://book.cakephp.org/phinx/0/en/seeding.html
*/
public function run()
{
$data = [
[
'descripcion' => 'Activa'
],
[
'descripcion' => 'Inactiva'
]
];
$this->table('tipos_estado_coneccion')
->insert($data)
->saveData();
}
}

41
api/phinx.php Normal file
View File

@ -0,0 +1,41 @@
<?php
return
[
'paths' => [
'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds'
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'development',
'production' => [
'adapter' => 'mysql',
'host' => 'db',
'name' => $_ENV['MYSQL_DATABASE'],
'user' => $_ENV['MYSQL_USER'],
'pass' => $_ENV['MYSQL_PASSWORD'],
'port' => '3306',
'charset' => 'utf8',
],
'development' => [
'adapter' => 'mysql',
'host' => 'db',
'name' => $_ENV['MYSQL_DATABASE'],
'user' => $_ENV['MYSQL_USER'],
'pass' => $_ENV['MYSQL_PASSWORD'],
'port' => '3306',
'charset' => 'utf8',
],
'testing' => [
'adapter' => 'mysql',
'host' => 'db',
'name' => $_ENV['MYSQL_DATABASE'],
'user' => $_ENV['MYSQL_USER'],
'pass' => $_ENV['MYSQL_PASSWORD'],
'port' => '3306',
'charset' => 'utf8',
]
],
'version_order' => 'creation'
];

2
api/php.ini Normal file
View File

@ -0,0 +1,2 @@
log_errors = true
error_log = /var/log/php/error.log

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
<?php
use Contabilidad\Common\Controller\Import;
$app->post('/import', Import::class);
$app->get('/import/uploads', [Import::class, 'uploads']);

View File

@ -14,6 +14,24 @@ return [
$arr['resources'],
'routes'
]);
$arr['public'] = implode(DIRECTORY_SEPARATOR, [
$arr['base'],
'public'
]);
$arr['uploads'] = implode(DIRECTORY_SEPARATOR, [
$arr['public'],
'uploads'
]);
$arr['pdfs'] = implode(DIRECTORY_SEPARATOR, [
$arr['uploads'],
'pdfs'
]);
return (object) $arr;
},
'urls' => function(Container $c) {
$arr = [
'python' => 'http://python:5000'
];
return (object) $arr;
}
];

View File

@ -0,0 +1,15 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
GuzzleHttp\Client::class => function(Container $c) {
return new GuzzleHttp\Client();
},
Contabilidad\Common\Service\PdfHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\PdfHandler($c->get(GuzzleHttp\Client::class), $c->get('folders')->pdfs, implode('/', [
$c->get('urls')->python,
'pdf',
'parse'
]));
}
];

View File

@ -6,6 +6,7 @@ use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $nombre
* @property TipoCategoria $tipo_id
*/
class Categoria extends Model {
public static $_table = 'categorias';
@ -18,6 +19,13 @@ class Categoria extends Model {
}
return $this->cuentas;
}
protected $tipo;
public function tipo() {
if ($this->tipo === null) {
$this->tipo = $this->childOf(TipoCategoria::class, [Model::SELF_KEY => 'tipo_id']);
}
return $this->tipo;
}
protected $saldo;
public function saldo() {
@ -34,6 +42,7 @@ class Categoria extends Model {
public function toArray(): array {
$arr = parent::toArray();
$arr['tipo'] = $this->tipo()->toArray();
$arr['saldo'] = $this->saldo();
$arr['saldoFormateado'] = '$' . number_format($this->saldo(), 0, ',', '.');
return $arr;

21
api/src/Coneccion.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $key
*/
class Coneccion extends Model {
public static $_table = 'conecciones';
protected static $fields = ['key'];
protected $estados;
public function estados() {
if ($this->estados === null) {
$this->estados = $this->parentOf(TipoEstadoConeccion::class, [Model::CHILD_KEY => 'coneccion_id']);
}
return $this->estados;
}
}

View File

@ -7,10 +7,11 @@ use ProVM\Common\Alias\Model;
* @property int $id
* @property string $nombre
* @property Categoria $categoria_id
* @property TipoCuenta $tipo_id
*/
class Cuenta extends Model {
public static $_table = 'cuentas';
protected static $fields = ['nombre', 'categoria_id'];
protected static $fields = ['nombre', 'categoria_id', 'tipo_id'];
protected $categoria;
public function categoria() {
@ -19,13 +20,12 @@ class Cuenta extends Model {
}
return $this->categoria;
}
protected $entradas;
public function entradas() {
if ($this->entradas === null) {
$this->entradas = $this->parentOf(Entrada::class, [Model::CHILD_KEY => 'cuenta_id']);
protected $cuenta;
public function cuenta() {
if ($this->cuenta === null) {
$this->cuenta = $this->childOf(TipoCuenta::class, [Model::SELF_KEY => 'tipo_id']);
}
return $this->entradas;
return $this->cuenta;
}
protected $cargos;

View File

@ -0,0 +1,30 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property Coneccion $coneccion_id
* @property DateTime $fecha
* @property TipoEstadoConeccion $tipo_id
*/
class EstadoConeccion extends Model {
public static $_table = 'estados_coneccion';
protected static $fields = ['coneccion_id', 'fecha', 'tipo_id'];
protected $coneccion;
public function coneccion() {
if ($this->coneccion === null) {
$this->coneccion = $this->childOf(Coneccion::class, [Model::SELF_KEY => 'coneccion_id']);
}
return $this->coneccion;
}
protected $tipo;
public function tipo() {
if ($this->tipo === null) {
$this->tipo = $this->childOf(TipoEstadoConeccion::class, [Model::SELF_KEY => 'tipo_id']);
}
return $this->tipo;
}
}

14
api/src/TipoCategoria.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $descripcion
* @property int $activo
*/
class TipoCategoria extends Model {
public static $_table = 'tipos_categoria';
protected static $fields = ['descripcion', 'activo'];
}

13
api/src/TipoCuenta.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $descripcion
*/
class TipoCuenta extends Model {
public static $_table = 'tipos_cuenta';
protected static $fields = ['descripcion'];
}

View File

@ -0,0 +1,13 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $descripcion
*/
class TipoEstadoConeccion extends Model {
public static $_table = 'tipos_estado_coneccion';
protected static $fields = ['descripcion'];
}

View File

@ -6,8 +6,8 @@ use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property Cuenta $desde_id
* @property Cuenta $hasta_id
* @property Cuenta $debito_id
* @property Cuenta $credito_id
* @property \DateTime $fecha
* @property string $glosa
* @property string $detalle
@ -15,21 +15,21 @@ use ProVM\Common\Alias\Model;
*/
class Transaccion extends Model {
public static $_table = 'transacciones';
protected static $fields = ['desde_id', 'hasta_id', 'fecha', 'glosa', 'detalle', 'valor'];
protected static $fields = ['debito_id', 'credito_id', 'fecha', 'glosa', 'detalle', 'valor'];
protected $desde;
public function desde() {
if ($this->desde === null) {
$this->desde = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'desde_id']);
protected $debito;
public function debito() {
if ($this->debito === null) {
$this->debito = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'debito_id']);
}
return $this->desde;
return $this->debito;
}
protected $hasta;
public function hasta() {
if ($this->hasta === null) {
$this->hasta = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'hasta_id']);
protected $credito;
public function credito() {
if ($this->credito === null) {
$this->credito = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'credito_id']);
}
return $this->hasta;
return $this->credito;
}
public function fecha(\DateTime $fecha = null) {
if ($fecha === null) {
@ -40,8 +40,8 @@ class Transaccion extends Model {
public function toArray(): array {
$arr = parent::toArray();
$arr['desde'] = $this->desde()->toArray();
$arr['hasta'] = $this->hasta()->toArray();
$arr['debito'] = $this->debito()->toArray();
$arr['credito'] = $this->credito()->toArray();
$arr['fechaFormateada'] = $this->fecha()->format('d-m-Y');
$arr['valorFormateado'] = '$' . number_format($this->valor, 0, ',', '.');
return $arr;

View File

@ -2,44 +2,62 @@ version: '3'
services:
api:
restart: unless-stopped
image: php
build:
context: api
env_file: .env
volumes:
- ./api/:/app/
- ./api/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./logs/api/php/:/var/log/php/
api-proxy:
restart: unless-stopped
image: nginx
ports:
- 9001:80
- "9001:80"
volumes:
- ./api/nginx.conf:/etc/nginx/conf.d/default.conf
- ./logs/api/:/var/log/nginx/
- ./api/:/app/
db:
restart: unless-stopped
image: mariadb
env_file: .env
volumes:
- contabilidad_data:/var/lib/mysql
adminer:
restart: unless-stopped
image: adminer
ports:
- 9002:8080
- "9002:8080"
ui:
restart: unless-stopped
image: php-ui
build:
context: ui
volumes:
- ./ui/:/app/
ui-proxy:
restart: unless-stopped
image: nginx
ports:
- 9000:80
- "9000:80"
volumes:
- ./ui/nginx.conf:/etc/nginx/conf.d/default.conf
- ./logs/ui/:/var/log/nginx/
- ./ui/:/app/
python:
restart: unless-stopped
build:
context: ./python
volumes:
- ./python/src/:/app/src/
- ./python/config/:/app/config/
- ./api/public/uploads/pdfs/:/app/data/
- ./logs/python/:/var/log/python/
volumes:
contabilidad_data:

1
python/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/__pycache__/

3
python/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
python/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (contabilidad)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
python/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/python.iml" filepath="$PROJECT_DIR$/.idea/python.iml" />
</modules>
</component>
</project>

8
python/.idea/python.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.9 (contabilidad)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
python/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

15
python/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM python
RUN apt-get update -y && apt-get install -y ghostscript python3-tk libgl-dev
RUN pip install flask pyyaml pypdf4 gunicorn camelot-py[cv] pikepdf
WORKDIR /app
COPY ./src/ /app/src/
EXPOSE 5000
WORKDIR /app/src
CMD ["gunicorn", "-b 0.0.0.0:5000", "app:app"]

View File

@ -0,0 +1,3 @@
passwords:
- 0839
- 159608395

Binary file not shown.

Binary file not shown.

3
python/src/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="property.tolist" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
python/src/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (python)" project-jdk-type="Python SDK" />
</project>

8
python/src/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/src.iml" filepath="$PROJECT_DIR$/.idea/src.iml" />
</modules>
</component>
</project>

8
python/src/.idea/src.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.9 (python)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
python/src/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

56
python/src/app.py Normal file
View File

@ -0,0 +1,56 @@
import io
import json
import os
import sys
from flask import Flask, request
import contabilidad.pdf as pdf
import contabilidad.passwords as passwords
import contabilidad.log as log
import contabilidad.text_handler as th
app = Flask(__name__)
log.logging['filename'] = '/var/log/python/contabilidad.log'
@app.route('/pdf/parse', methods=['POST'])
def pdf_parse():
data = request.get_json()
if not isinstance(data['files'], list):
data['files'] = [data['files']]
password_file = '/app/config/.passwords.yml'
pwds = passwords.get_passwords(password_file)
output = []
for file in data['files']:
filename = os.path.realpath(os.path.join('/app/data', file['filename']))
t = file['filename'].split('.')
temp = os.path.realpath(os.path.join('/app/data', t[0] + '-temp.pdf'))
for p in pwds:
if not pdf.check_password(filename, p):
continue
pdf.remove_encryption(filename, p, temp)
obj = pdf.get_data(temp)
outputs = []
for o in obj:
out = json.loads(o.df.to_json(orient='records'))
if out[0]['0'] == 'FECHA':
for i, line in enumerate(out):
if 'FECHA' in line['0'] or 'ACTUALICE' in line['0']:
continue
if line['0'] == '':
spl = line['1'].split(' ')
else:
spl = line['0'].split(' ')
line['0'] = ' '.join(spl[:3])
line['1'] = ' '.join(spl[3:])
out[i] = line
outputs.append(out)
os.remove(temp)
output.append({'filename': file['filename'], 'text': outputs})
return json.dumps(output)
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)

Binary file not shown.

View File

@ -0,0 +1,19 @@
import time
logging = {
'filename': '/var/log/python/error.log'
}
class LOG_LEVEL:
INFO = 'INFO'
WARNING = 'WARNING'
DEBUG = 'DEBUG'
ERROR = 'ERROR'
def log(message, level=LOG_LEVEL.INFO):
filename = logging['filename']
with open(filename, 'a') as f:
f.write(time.strftime('[%Y-%m-%d %H:%M:%S] ') + ' - ' + level + ': ' + message)

View File

@ -0,0 +1,6 @@
import yaml
def get_passwords(filename):
with open(filename, 'r') as f:
return yaml.load(f, Loader=yaml.Loader)['passwords']

View File

@ -0,0 +1,51 @@
import camelot
import PyPDF4
import pikepdf
def is_encrypted(filename):
with open(filename, 'rb') as file:
reader = PyPDF4.PdfFileReader(file)
return reader.getIsEncrypted()
def check_password(filename, password):
if not is_encrypted(filename):
return True
with open(filename, 'rb') as file:
reader = PyPDF4.PdfFileReader(file)
status = reader.decrypt(str(password))
if status == 0:
return False
return reader.getIsEncrypted()
def remove_encryption(filename, password, new_name):
pdf = pikepdf.open(filename, password=str(password))
if '.pdf' not in new_name:
new_name += '.pdf'
pdf.save(new_name)
def get_pdf(file, password=''):
reader = PyPDF4.PdfFileReader(file)
if reader.getIsEncrypted() and password != '':
status = reader.decrypt(password=str(password))
if status == 0:
return None
return reader
def get_text(filename, password=''):
with open(filename, 'rb') as f:
reader = get_pdf(f, password)
if reader is None:
return None
texts = []
for p in range(0, reader.getNumPages()):
texts.append(reader.getPage(p).extractText())
return texts
def get_data(filename):
return camelot.read_pdf(filename, pages='all', output_format='json')

View File

@ -0,0 +1,54 @@
import argparse
import yaml
import PyPDF4
import httpx
def get_pdf(file, password=''):
reader = PyPDF4.PdfFileReader(file)
if password != '':
status = reader.decrypt(password=password)
if status == 0:
print('Not decrypted')
return reader
def send_to_parser(url, text):
res = httpx.post(url, data={'to_parse': text})
return {'status': res.status_code, 'text': res.json()}
def get_text(filename, password=''):
with open(filename, 'rb') as f:
reader = get_pdf(f, password)
texts = []
for p in range(0, reader.getNumPages()):
texts.append(reader.getPage(p).extractText())
return "\n".join(texts)
def get_config(filename):
with open(filename, 'r') as f:
return yaml.load(f, Loader=yaml.Loader)
def main(args):
password = ''
if args.config_file is not None:
config = get_config(args.config_file)
password = config['password']
if args.password is not None:
password = args.password
text = get_text(args.filename, password)
res = send_to_parser(args.url, text)
print(res)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--filename', type=str)
parser.add_argument('-p', '--password', type=str)
parser.add_argument('-c', '--config_file', type=str)
parser.add_argument('-u', '--url', type=str)
_args = parser.parse_args()
main(_args)

View File

@ -0,0 +1,90 @@
def text_cleanup(text, filename: str = None):
if isinstance(text, list):
output = []
for t in text:
output.append(text_cleanup(t, filename=filename))
return output
if filename is None:
return text
if 'bice' in filename.lower():
return bice(text)
if 'scotiabank' in filename.lower():
return scotiabank(text)
return text
def bice(text):
lines = text.split("\n\n\n")
print(lines)
return text
def scotiabank(text):
words = text.split("\n")
output = [words[0]]
output = output + extract_from_to(words, 'No. CTA.', end='VENCIMIENTO LINEA DE CREDITO', line_length=3)
output = output + extract_from_to(words, 'VENCIMIENTO LINEA DE CREDITO',
end='NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', line_length=2)
output = output + extract_from_to(words, 'NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', end='SALDO ANTERIOR',
line_length=1)
output = output + extract_from_to(words, 'SALDO ANTERIOR', end='FECHA', line_length=4)
output = output + extract_data(words, 'FECHA', end='ACTUALICE SIEMPRE ANTECEDENTES LEGALES, ', line_length=6,
merge_list=[['DOCTO', 'No.'], ['SALDO', 'DIARIO']])
[print(li) for li in output]
return text
def extract_from_to(word_list, start, line_length, end: str = None, merge_list=None):
if end is not None:
return extract_by_line(word_list[word_list.index(start):word_list.index(end)], line_length, merge_list)
return extract_by_line(word_list[word_list.index(start):], line_length, merge_list)
def extract_by_line(word_list, line_length, merge_list=None):
if merge_list is not None:
word_list = merge_words(word_list, merge_list)
output = []
line = []
for k, w in enumerate(word_list):
if k > 0 and k % line_length == 0:
output.append(line)
line = []
line.append(w)
output.append(line)
return output
def merge_words(word_list, merge_list):
for m in merge_list:
i = word_list.index(m[0])
word_list = word_list[:i] + ["\n".join(m)] + word_list[i+len(m):]
return word_list
def extract_data(word_list, start, line_length, end=None, merge_list=None, date_sep='/'):
word_list = word_list[word_list.index(start):]
if end is not None:
word_list = word_list[:word_list.index(end)]
if merge_list is not None:
word_list = merge_words(word_list, merge_list)
output = []
line = []
line_num = 0
col = 0
for k, w in enumerate(word_list):
if col > 0 and col % line_length == 0:
output.append(line)
line = []
col = 0
if col > 0 and date_sep in w and len(line) < line_length:
cnt = 0
for i in range(len(line), line_length):
line.append('')
cnt += 1
output.append(line)
line = [w]
col += cnt + 1
continue
line.append(w)
col += 1
return output

24
python/src/main.py Normal file
View File

@ -0,0 +1,24 @@
import argparse
import os
import contabilidad.pdf as pdf
import contabilidad.text_handler as th
def main(args):
filename = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data', args.filename))
temp = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data', args.temp_filename))
pdf.remove_encryption(filename, args.password, temp)
obj = pdf.get_data(temp)
obj = pdf.get_text(filename, args.password)
text = th.text_cleanup(obj, filename=str(args.filename))
os.remove(temp)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--filename', type=str)
parser.add_argument('-p', '--password', type=str, default='')
parser.add_argument('-t', '--temp_filename', type=str)
_args = parser.parse_args()
main(_args)

View File

@ -0,0 +1,35 @@
# Tests
### 1. Conductor
+ Set start event
+ Get ready events
+ Set first step event
+ Get first step ready
+ Set second step event
+ Get second step ready
### 2. Email
+ Connect to IMAP
+ Wrong data
+ Wrong configuration
+ Get mailboxes
+ Get mail ids with search
+ Get mails by id
+ Get mail by id
+ Get attachment
+ Close connection
### 3. API Sender
+ Get attachments
+ Process
+ Send to API
## Steps
1. Start
+ Connect
+ Standby
2. Find emails, get attachments
3. Process attachments
4. Send to API
5. Close

12
worker/main.py Normal file
View File

@ -0,0 +1,12 @@
from threading import Thread
import httpx
class Worker(Thread):
def __init__(self, settings):
self.settings = settings
def run():
while True:
if self.stop_event.isSet():
break