Add youtrack service.

master
Lio Novelli 2021-09-20 01:08:42 +02:00
parent 0346b9f29d
commit 9cf4d23012
14 changed files with 516 additions and 371 deletions

View File

@ -64,3 +64,10 @@
- Choices (~new ChoiceQuestion~) - Choices (~new ChoiceQuestion~)
- ~addOption('config')~ - ~addOption('config')~
** API calls
*** Get csv file
curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv

View File

@ -18,8 +18,7 @@
"guzzlehttp/guzzle": "^7.3", "guzzlehttp/guzzle": "^7.3",
"php-di/php-di": "^6.3", "php-di/php-di": "^6.3",
"symfony/yaml": "^5.2", "symfony/yaml": "^5.2",
"mpdf/mpdf": "^8.0", "mpdf/mpdf": "^8.0"
"symfony/translation": "^5.2"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

139
app/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e106a8658dd1c87ae4ec9212251d4943", "content-hash": "e6f2abc31fc53724d858e127ce02978e",
"packages": [ "packages": [
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
@ -1506,143 +1506,6 @@
], ],
"time": "2021-03-17T17:12:15+00:00" "time": "2021-03-17T17:12:15+00:00"
}, },
{
"name": "symfony/translation",
"version": "v5.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "2cc7f45d96db9adfcf89adf4401d9dfed509f4e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/2cc7f45d96db9adfcf89adf4401d9dfed509f4e1",
"reference": "2cc7f45d96db9adfcf89adf4401d9dfed509f4e1",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "^1.15",
"symfony/translation-contracts": "^2.3"
},
"conflict": {
"symfony/config": "<4.4",
"symfony/dependency-injection": "<5.0",
"symfony/http-kernel": "<5.0",
"symfony/twig-bundle": "<5.0",
"symfony/yaml": "<4.4"
},
"provide": {
"symfony/translation-implementation": "2.3"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "^4.4|^5.0",
"symfony/console": "^4.4|^5.0",
"symfony/dependency-injection": "^5.0",
"symfony/finder": "^4.4|^5.0",
"symfony/http-kernel": "^5.0",
"symfony/intl": "^4.4|^5.0",
"symfony/service-contracts": "^1.1.2|^2",
"symfony/yaml": "^4.4|^5.0"
},
"suggest": {
"psr/log-implementation": "To use logging capability in translator",
"symfony/config": "",
"symfony/yaml": ""
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\Translation\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"time": "2021-03-23T19:33:48+00:00"
},
{
"name": "symfony/translation-contracts",
"version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
"reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/e2eaa60b558f26a4b0354e1bbb25636efaaad105",
"reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105",
"shasum": ""
},
"require": {
"php": ">=7.2.5"
},
"suggest": {
"symfony/translation-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.3-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Translation\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to translation",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"time": "2020-09-28T13:05:58+00:00"
},
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
"version": "v5.2.5", "version": "v5.2.5",

View File

@ -4,6 +4,13 @@
tracking service: tracking service:
youtrack: youtrack:
auth token: '<value from youtrack hub>' auth token: '<value from youtrack hub>'
# reports:
# report short name:
# table:
# header:
# # overrides for table header
# hours: Quantity
# source: <youtrack-url>
projects: projects:
'<short name of first project>': '<short name of first project>':
name: '<Project long name>' name: '<Project long name>'
@ -14,3 +21,10 @@ projects:
time column: 4 time column: 4
# time format m - minutes, h - hours # time format m - minutes, h - hours
time format: 'm' time format: 'm'
labels:
project: Project
# hours: Quantity
hours: Hours
rate: 'Price per hour'
price: Price
locale: 'en_GB'

View File

@ -9,17 +9,34 @@ use RprtCli\Utils\Configuration\ConfigurationService;
use RprtCli\Utils\CsvReport\CsvReport; use RprtCli\Utils\CsvReport\CsvReport;
use RprtCli\Utils\CsvReport\CsvReportInterface; use RprtCli\Utils\CsvReport\CsvReportInterface;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
use RprtCli\Utils\TimeTrackingServices\YoutrackService;
# use Symfony\Component\Translation\Translator;
#use Symfony\Component\Translation\Loader\PoFileLoader;
return [ return [
'config.file' => 'rprt.config.yml', 'config.file' => 'rprt.config.yml',
'config.path' => '~/.config/rprt-cli/', 'config.path' => '~/.config/rprt-cli/',
'guzzle' => create()->constructor(Client::class), 'default_locale' => 'en',
// 'translator' => ['default_path' => '%kernel.project_dir%/translations'],
// 'guzzle' => create()->constructor(Client::class),
'guzzle' => get(Client::class),
ConfigurationInterface::class => get(ConfigurationService::class), ConfigurationInterface::class => get(ConfigurationService::class),
ConfigurationService::class => create()->constructor( ConfigurationService::class => create()->constructor(
get('config.path'), get('config.path'),
get('config.file') get('config.file')
), ),
'config.service' => get(ConfigurationInterface::class), 'config.service' => get(ConfigurationInterface::class),
YoutrackInterface::class => get(YoutrackService::class),
YoutrackService::class => create()->constructor(
get('config.service'),
get('guzzle')
),
'youtrack.service' => get(YoutrackInterface::class),
// 'locale' => get('config.service')->method('get', 'en'),
// Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader),
// 'translator' => get(Translator::class),
CsvReportInterface::class => get(CsvReport::class), CsvReportInterface::class => get(CsvReport::class),
CsvReport::class => create()->constructor( CsvReport::class => create()->constructor(
get('config.service') get('config.service')
@ -27,6 +44,7 @@ return [
'csv.report' => get(CsvReportInterface::class), 'csv.report' => get(CsvReportInterface::class),
RprtCommand::class => create()->constructor( RprtCommand::class => create()->constructor(
get('csv.report'), get('csv.report'),
get('config.service') get('config.service'),
) get('youtrack.service')
),
]; ];

View File

@ -10,6 +10,7 @@ require __DIR__ . '/vendor/autoload.php';
$builder = new ContainerBuilder(); $builder = new ContainerBuilder();
$builder->addDefinitions('dependencies.php'); $builder->addDefinitions('dependencies.php');
$container = $builder->build(); $container = $builder->build();
$application = new Application(); $application = new Application();
$rprtCommand = $container->get(RprtCommand::class); $rprtCommand = $container->get(RprtCommand::class);

View File

@ -1,105 +1,159 @@
<?php <?php
declare(strict_types=1);
// src/Commands/RprtCommand.php; // src/Commands/RprtCommand.php;
namespace RprtCli\Commands; namespace RprtCli\Commands;
use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\CsvReport\CsvReportInterface; use RprtCli\Utils\CsvReport\CsvReportInterface;
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
// use Symfony\Contracts\Translation\TranslatorInterface;
class RprtCommand extends Command { use function var_dump;
protected $csv; /**
* Main file - rprt command.
*/
class RprtCommand extends Command
{
protected $csv;
protected $configuration; protected $configuration;
public function __construct(CsvReportInterface $csv, ConfigurationInterface $configuration) { protected $youtrack;
$this->csv = $csv;
$this->configuration = $configuration;
parent::__construct();
}
/** public function __construct(
* Get configuration. CsvReportInterface $csv,
*/ ConfigurationInterface $configuration,
protected function configure(): void { YoutrackInterface $youtrack,
$this->setName('rprt'); ?string $name = null
$this->setDescription('Generate monthly report'); ) {
// @TODO $this->addUsage(''); $this->csv = $csv;
$this->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'Specify the input csv file to generate report from.'); $this->configuration = $configuration;
} $this->youtrack = $youtrack;
parent::__construct($name);
protected function execute(InputInterface $input, OutputInterface $output): int {
if ($file = $input->getOption('file')) {
$data = $this->csv->getReportData($file);
$table = $this->generateTable($output, $data);
$table->render();
return Command::SUCCESS;
} }
$this->dummyOutput($input, $output); /**
return Command::SUCCESS; * Get configuration.
} */
protected function configure() : void
{
$this->setName('rprt');
$this->setDescription('Generate monthly report');
// @TODO $this->addUsage('');
$this->addOption(
'file',
'f',
InputOption::VALUE_REQUIRED,
'Specify the input csv file to generate report from.'
);
$this->addOption(
'youtrack',
'y',
InputOption::VALUE_NONE,
'Use youtrack api to get a report. If this option is used --file does not have any effect.'
);
$this->addOption(
'pdf',
'p',
InputOption::VALUE_NONE,
'Create invoice pdf from template.'
);
$this->addOption(
'test',
't',
InputOption::VALUE_NONE,
'Test login into youtrack service.'
);
}
protected function execute(InputInterface $input, OutputInterface $output) : int
{
if ($input->getOption('test')) {
$test = $this->youtrack->testYoutrackapi();
$output->writeln($test);
}
if ($file = $input->getOption('file')) {
$data = $this->csv->getReportData($file);
$table = $this->generateTable($output, $data);
$table->render();
return Command::SUCCESS;
}
$this->dummyOutput($input, $output);
return Command::SUCCESS;
}
/** /**
* Create table from data that is already inline with configuration. * Create table from data that is already inline with configuration.
*/ */
protected function generateTable($output, $data) { protected function generateTable(OutputInterface $output, array $data) : Table
$table = new Table($output); {
$table->setHeaders(['Project', 'Hours', 'Rate', 'Price']); $table = new Table($output);
list($rows, $total_hours, $total_price) = [[], 0, 0]; $table->setHeaders([
$projects_config = $this->configuration->get('projects'); // $this->translator->trans('Project', [], 'messages', 'sl_SI'),
foreach ($projects_config as $name => $config) { // $this->translator->trans('Hours', [], 'messages', 'sl_SI'),
if (!isset($data[$name])) { // $this->translator->trans('Rate'),
// @TODO Proper error handling. // $this->translator->trans('Price'),
var_dump('Project ' . $name . ' is not set!'); 'Project', 'Hours', 'Rate', 'Price',
continue; ]);
} [$rows, $totalHours, $totalPrice] = [[], 0, 0];
$hours = $data[$name]; $projectsConfig = $this->configuration->get('projects');
if ($config['time_format'] === 'm') { foreach ($projectsConfig as $name => $config) {
$hours = $hours/60; if (! isset($data[$name])) {
} // @TODO Proper error handling.
$price = $hours * (int) $config['price']; var_dump('Project ' . $name . ' is not set!');
$row = [ continue;
$config['name'], }
$hours, $hours = $data[$name];
$config['price'], if ($config['time_format'] === 'm') {
$hours * $config['price'], $hours /= 60;
]; }
$rows[] = $row; $price = $hours * (int) $config['price'];
$total_hours += $hours; $row = [
$total_price += $price; $config['name'],
$hours,
$config['price'],
$hours * $config['price'],
];
$rows[] = $row;
$totalHours += $hours;
$totalPrice += $price;
}
$rows[] = new TableSeparator();
// @TODO Check rate in final result.
// $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice];
$rows[] = ['Sum', $totalHours, $config['price'], $totalPrice];
$table->setRows($rows);
return $table;
} }
$rows[] = new TableSeparator();
// @TODO Check rate in final result.
$rows[] = ['Zusamen', $total_hours, $config['price'], $total_price];
$table->setRows($rows);
return $table;
}
/** /**
* Dummy output for testing. * Dummy output for testing.
*/ */
protected function dummyOutput(InputInterface $input, OutputInterface $output): void { protected function dummyOutput(InputInterface $input, OutputInterface $output) : void
$output->writeln('I will output a nice table.'); {
$table = New Table($output); // $txt = $this->translator->trans('From [start-date] to [end-date].', [], 'rprt', 'sl_SI');
$table->setHeaders(['Project', 'Hours', 'Price']); // $output->writeln($txt);
$table->setRows([ $table = new Table($output);
['LDP', 100, 2600], $table->setHeaders(['Project', 'Hours', 'Price']);
['WV', 50, 1300], $table->setRows([
new TableSeparator(), ['LDP', 100, 2600],
['Zusamen', 150, 3900], ['WV', 50, 1300],
]); new TableSeparator(),
// $table->setStyle('borderless'); ['Zusamen', 150, 3900],
$table->render(); ]);
// $table->setStyle('borderless');
} $table->render();
}
} }

View File

@ -1,32 +1,25 @@
<?php <?php
declare(strict_types=1);
// src/Utils/Configuration/ConfigurationInterface.php // src/Utils/Configuration/ConfigurationInterface.php
namespace RprtCli\Utils\Configuration; namespace RprtCli\Utils\Configuration;
interface ConfigurationInterface interface ConfigurationInterface
{ {
/**
* Checks for config file.
*
* @return string|bool
* Full path to config file or FALSE if it doesn't exist.
*/
// function findConfig($file);
/** /**
* Get and read the configuration from file. * Get and read the configuration from file.
*/ */
function getConfig(); public function getConfig() : bool;
/** /**
* Get a specific configuration for key. * Get a specific configuration for key.
* *
* @param string $key * @param string $key
* Config key. * Config key.
* @param mixed $default * @param null|mixed $default
* Default value if config for key is not yet specified. * Default value if config for key is not yet specified.
*
* @return mixed * @return mixed
* Data. * Data.
*/ */
@ -38,5 +31,5 @@ interface ConfigurationInterface
* @param string $key * @param string $key
* Key to check for. * Key to check for.
*/ */
public function exists($key); public function exists($key) : bool;
} }

View File

@ -1,12 +1,17 @@
<?php <?php
declare(strict_types=1);
// src/Utils\Configuration/ConfigurationService.php // src/Utils\Configuration/ConfigurationService.php
namespace RprtCli\Utils\Configuration; namespace RprtCli\Utils\Configuration;
use RprtCli\Utils\Configuration\ConfigurationInterface;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use function explode;
use function file_exists;
use function var_dump;
/** /**
* Read and write configuration. * Read and write configuration.
* *
@ -17,99 +22,111 @@ use Symfony\Component\Yaml\Yaml;
*/ */
class ConfigurationService implements ConfigurationInterface class ConfigurationService implements ConfigurationInterface
{ {
protected const PATHS = [
'/.',
'/.config/rprt-cli/',
'/.rprt/',
];
const PATHS = [ protected $data;
'/.',
'/.config/rprt-cli/',
'/.rprt/',
];
protected $data; protected $default = null;
protected $default = null; protected $configFilePath;
protected $configFilePath; protected $configFileName;
protected $configFileName;
/**
* Yaml service.
*
* @var \Symfony\Component\Yaml\Yaml::parseFile;
*/
protected $yamlParseFile;
/** /**
* Construct method. * Construct method.
*/ */
function __construct(string $filepath, string $filename) { public function __construct(string $filepath, string $filename)
$file = $filepath . $filename; {
$this->configFileName = $filename; $file = $filepath . $filename;
$this->configFilePath = $this->findConfig($file); $this->configFileName = $filename;
if ($this->configFilePath) { $this->configFilePath = $this->findConfig($file);
$this->getConfig(); if ($this->configFilePath) {
$this->
getConfig();
}
} }
}
/** /**
* Checks for config file. * Checks for config file.
* *
* @param string $filename
* Name of the configuration file.
* @return string|bool * @return string|bool
* Full path to config file or FALSE if it doesn't exist. * Full path to config file or FALSE if it doesn't exist.
*/ */
public function findConfig($filename) { public function findConfig($filename)
if (file_exists($filename)) { {
return $filename; if (file_exists($filename)) {
return $filename;
}
foreach (self::PATHS as $path) {
$fullPath = $_SERVER['HOME'] . $path . $this->configFileName;
if (file_exists($fullPath)) {
return $fullPath;
}
}
// @TODO This should be some kind of error!
var_dump('Config File Not Found!');
return false;
} }
foreach (self::PATHS as $path) {
$fullPath = $_SERVER['HOME'] . $path . $this->configFileName;
if (file_exists($fullPath)) {
return $fullPath;
}
}
// @TODO This should be some kind of error!
var_dump('Config File Not Found!');
return FALSE;
}
/** /**
* Get and read the configuration from file. * Get and read the configuration from file.
*/ */
public function getConfig() { public function getConfig() : bool
if ($this->configFilePath) { {
$config = Yaml::parseFile($this->configFilePath); if ($this->configFilePath) {
$this->data = $config; $config = Yaml::parseFile($this->configFilePath);
return TRUE; $this->data = $config;
return true;
}
// Maybe write an exception for missing config.
// Ask for reconfiguration.
// @TODO This should be some kind of error!
var_dump('Config File Path not found!');
return false;
} }
// Maybe write an exception for missing config.
// Ask for reconfiguration.
// @TODO This should be some kind of error!
var_dump('Config File Path not found!');
return FALSE;
}
/** /**
* {@inheritdoc} * Get a specific configuration for key.
*/ *
public function get($key, $default = null) { * @param string $key
$this->default = $default; * Config key.
$segments = explode('.', $key); * @param null|mixed $default
$data = $this->data; * Default value if config for key is not yet specified.
foreach ($segments as $segment) { * @return mixed
if (isset($data[$segment])) { * Data.
$data = $data[$segment]; */
} else { public function get($key, $default = null)
$data = $this->default; {
break; $this->default = $default;
} $segments = explode('.', $key);
$data = $this->data;
foreach ($segments as $segment) {
if (isset($data[$segment])) {
$data = $data[$segment];
} else {
$data = $this->default;
break;
}
}
return $data;
} }
return $data;
}
/** /**
* {@inheritdoc} * Checks if key exists in the configuration file.
*/ *
public function exists($key) { * @param string $key
return $this->get($key) !== $this->default; * Key to check for.
} *
* Value of configuration key is not equal to default.
*/
public function exists($key) : bool
{
return $this->get($key) !== $this->default;
}
} }

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
// src/Utils/Configuration/TranslationService.php
use Symfony\Component\Translation\Translator;
use RprtCli\Utils\Configuration\ConfigurationInterface;
class TranslationService {
protected $config;
public function __construct(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
}

View File

@ -1,75 +1,102 @@
<?php <?php
declare(strict_types=1);
namespace RprtCli\Utils\CsvReport; namespace RprtCli\Utils\CsvReport;
use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\CsvReport\CsvReportInterface;
class CsvReport implements CsvReportInterface { use function array_key_first;
use function array_keys;
use function fgetcsv;
use function fopen;
use function preg_match;
use function reset;
protected $configurationService; /**
* Creates a report of projects and hours.
*/
class CsvReport implements CsvReportInterface
{
/**
* A configuration service.
*
* @var ConfigurationInterface
*/
protected $configurationService;
function __construct(ConfigurationInterface $config) { public function __construct(ConfigurationInterface $config)
$this->configurationService = $config; {
} $this->configurationService = $config;
public function getReportData(string $file_path): array {
$output = [];
// @TODO replace with config service.
// $config = $this->dummyConfig()['projects'];
$config = $this->configurationService->get('projects');
foreach (array_keys($config) as $key) {
$output[$key] = 0;
} }
if ($file = fopen($file_path, 'r')) {
while (($line = fgetcsv($file)) !== FALSE) { /**
$parsed = $this->parseCsvFile($line); * {@inheritdoc}
// $key = reset(array_keys($parsed)); */
$key = array_key_first($parsed); public function getReportData(string $filePath) : array
if (isset($output[$key])) { {
$output[$key] += (int) reset($parsed); $output = [];
// @TODO replace with config service.
// $config = $this->dummyConfig()['projects'];
$config = $this->configurationService->get('projects');
foreach (array_keys($config) as $key) {
$output[$key] = 0;
} }
} if ($file = fopen($filePath, 'r')) {
while (($line = fgetcsv($file)) !== false) {
$parsed = $this->parseCsvFile($line);
// $key = reset(array_keys($parsed));
$key = array_key_first($parsed);
if (isset($output[$key])) {
$output[$key] += (int) reset($parsed);
}
}
}
return $output;
} }
return $output;
}
protected function parseCsvFile(array $raw_data): array { /**
// $config = $this->dummyConfig(); * Get correct values from the raw data lines of csv.
$config = $this->configurationService->get('projects'); *
// var_dump($raw_data); *
foreach ($config as $key => $project) { * Columns with data are specified in config.
if (preg_match('/'.$project['pattern'].'/', $raw_data[1])) { *
return [$key => $raw_data[4]]; * Project key and unit of time spent.
} */
protected function parseCsvFile(array $rawData) : array
{
$config = $this->configurationService->get('projects');
foreach ($config as $key => $project) {
if (preg_match('/' . $project['pattern'] . '/', $rawData[1])) {
return [$key => $rawData[4]];
}
}
return [];
} }
return [];
}
protected function dummyConfig(): array { protected function dummyConfig() : array
$config = [ {
'projects' => [ return [
'LDP' => [ 'projects' => [
'name' => 'lupus.digital', 'LDP' => [
'pattern' => 'LDP-[0-9]+', 'name' => 'lupus.digital',
'price' => 26, 'pattern' => 'LDP-[0-9]+',
'price' => 26,
// optional specify columns // optional specify columns
], ],
'WV' => [ 'WV' => [
'name' => 'Wirtschaftsverlag', 'name' => 'Wirtschaftsverlag',
'pattern' => 'WV-[0-9]+', 'pattern' => 'WV-[0-9]+',
'price' => 26, 'price' => 26,
// optional specify columns // optional specify columns
], ],
'Other' => [ 'Other' => [
'name' => 'Other projects', 'name' => 'Other projects',
'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)', 'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)',
'price' => 26, 'price' => 26,
// optional specify columns // optional specify columns
], ],
], ],
]; ];
}
return $config;
}
} }

View File

@ -1,22 +1,21 @@
<?php <?php
namespace RprtCli\Utils\CsvReport; declare(strict_types=1);
namespace RprtCli\Utils\CsvReport;
/** /**
* Handles creating report data from csv file downloaded from youtrack service. * Handles creating report data from csv file downloaded from youtrack service.
*/ */
interface CsvReportInterface { interface CsvReportInterface
{
/**
* Gets project configuration and parses the data.
*
* Calculate number of hours per project.
*/
// protected function parseCsvFile(array $data): array;
/** /**
* Returns array of hours per configured projects. * Returns array of hours per configured projects.
*
* @todo - get data from variable.
*
*
* Project key as key and number of hours as value.
*/ */
public function getReportData(string $file_path): array; public function getReportData(string $filePath) : array;
} }

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices;
interface YoutrackInterface
{
/**
* Check if client can sign into youtrack with provided token.
*/
public function testYoutrackapi() : ?string;
/**
* Get the id of the report from configuration.
*/
public function getReportId() : ?string;
/**
* Downloads report and returns file path.
*
* @param string $report_id
* Youtrack internal report id.
*
* @return NULL|string
* If fetch was unsuccssefull return false, otherwise the file path.
*/
public function downloadReport(string $report_id) : ?string;
}

View File

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
// src/Utils/TimeTrackingServices/YoutrackService.php
namespace RprtCli\Utils\TimeTrackingServices;
use GuzzleHttp\ClientInterface;
use RprtCli\Utils\Configuration\ConfigurationInterface;
class YoutrackService implements YoutrackInterface
{
protected $config;
protected $httpClient;
public function __construct(ConfigurationInterface $config, ClientInterface $http_client)
{
$this->config = $config;
$this->httpClient = $http_client;
}
public function testYoutrackapi(): ?string
{
// Get base url from config or add input.
// Get token or add input.
$path = 'youtrack/api/admin/users/me';
$yt_url = $this->getYtUrl($path);
$yt_token = $this->getYtToken();
$query = ['fields' => 'id,email,fullName'];
$headers = [
"Authorization" => "Bearer $yt_token",
'Cache-Control' =>'no-cache',
];
$me_response = $this->httpClient->request('GET', $yt_url, [
'query' => $query,
'headers' => $headers
]);
$test = (string) $me_response->getBody()->getContents();
$me_json = (array) json_decode($test);
if ($me_json && isset($me_json['fullName'])) {
return $me_json['fullName'];
}
return NULL;
}
public function getReportId(): ?string
{
// --report option value should take precedence.
// @TODO error handling.
$yt_report_id = $this->config->get('youtrack.report_id');
if (!$yt_report_id) {
$yt_report_id = readline('Enter the report id: ');
}
return $yt_report_id;
}
public function downloadReport(string $report_id): ?string
{
$path = "youtrack/api/reports/$report_id/export/csv";
$query = ['$top' => -1];
$yt_token = $this->getYtToken();
$headers = [
'Accept' => 'text/plain, */*',
'Accept-Language' => 'en-US,en;q=0.5',
"Authorization" => "Bearer $yt_token",
];
$csv_response = $this->httpClient->request('GET', $this->getYtUrl($path), [
'headers' => $headers,
'query' => $query,
]);
// Write csv response test into temporary file.
$csv_file = tempnam(sys_get_temp_dir(), 'yt_csv_');
file_put_contents($csv_file, $csv_response->getBody());
return $csv_file;
}
protected function getYtToken(): string {
$yt_token = $this->config->get('tracking_service.youtrack.auth_token', FALSE); //
if (!$yt_token) {
$yt_token = readline('Enter your youtrack authentication token: ');
}
return $yt_token;
}
protected function getYtUrl(string $path = ''): ?string {
$yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE);
if (!$yt_base_url) {
$yt_base_url = readline('Enter base url for of the youtrack service: ');
}
if (!empty($path)) {
$yt_base_url = $yt_base_url . '/' . trim($path, '/');
}
return $yt_base_url;
}
}