Skip to content

Commit

Permalink
feat: add better exceptions & improve readability by refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
kauffinger committed Dec 22, 2024
1 parent 578d9dd commit 6ffc08e
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 28 deletions.
34 changes: 34 additions & 0 deletions src/Concerns/ManagesDependencies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Kauffinger\Pyman\Concerns;

use Kauffinger\Pyman\DependencyManager;

trait ManagesDependencies
{
private DependencyManager $dependencies;

public function addDependency(string $dependency): self
{
$this->dependencies->add($dependency);

return $this;
}

public function clearDependencies(): self
{
$this->dependencies->clear();

return $this;
}

/**
* @return array<string>
*/
public function getDependencies(): array
{
return $this->dependencies->get();
}
}
48 changes: 48 additions & 0 deletions src/DependencyManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Kauffinger\Pyman;

use Illuminate\Process\Factory;
use Kauffinger\Pyman\Exceptions\MissingDependencyException;

class DependencyManager
{
/**
* Holds list of dependencies that are required for the installation process.
*
* @param array<string> $dependencies
*/
public function __construct(private readonly Factory $processFactory, private array $dependencies = []) {}

public function add(string $dependency): void
{
$this->dependencies[] = $dependency;
}

public function clear(): void
{
$this->dependencies = [];
}

/**
* @return string[]
*/
public function get(): array
{
return $this->dependencies;
}

public function check(): void
{
foreach ($this->dependencies as $command) {
$result = $this->processFactory->newPendingProcess()
->run("command -v $command");

if (! $result->successful()) {
throw new MissingDependencyException("$command is required but not installed.");
}
}
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/FolderCouldNotBeCreatedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Kauffinger\Pyman\Exceptions;

class FolderCouldNotBeCreatedException extends PymanException
{
public function __construct(string $message)
{
parent::__construct($message);
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/MissingDependencyException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Kauffinger\Pyman\Exceptions;

class MissingDependencyException extends PymanException
{
public function __construct(string $message)
{
parent::__construct($message);
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/VenvManagementException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Kauffinger\Pyman\Exceptions;

class VenvManagementException extends PymanException
{
public function __construct(string $message)
{
parent::__construct($message);
}
}
47 changes: 20 additions & 27 deletions src/PythonEnvironmentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,29 @@

use Illuminate\Process\Factory;
use Illuminate\Process\PendingProcess;
use Kauffinger\Pyman\Concerns\ManagesDependencies;
use Kauffinger\Pyman\Exceptions\FolderCouldNotBeCreatedException;
use Kauffinger\Pyman\Exceptions\PymanException;
use Kauffinger\Pyman\Exceptions\VenvManagementException;

class PythonEnvironmentManager
{
use ManagesDependencies;

private readonly string $pythonDir;

private readonly RequirementsManager $requirements;

public function __construct(string $basePath, private readonly Factory $processFactory)
{
$this->pythonDir = realpath($basePath) ?: $basePath;

$this->dependencies = new DependencyManager($this->processFactory, [
'python3',
'pip3',
]);

$this->requirements = new RequirementsManager($this->processFactory, $this->pythonDir);
}

/**
Expand All @@ -27,22 +41,15 @@ public function setup(): void
$this->checkPythonRequirements();
$this->createPythonDirectory();
$this->createVirtualEnvironment();
$this->installDependencies();
$this->installRequirements();
}

/**
* @throws PymanException
*/
private function checkPythonRequirements(): void
{
$commands = ['python3', 'pip3'];

foreach ($commands as $command) {
$result = $this->makeProcess()->run("command -v $command");
if (! $result->successful()) {
throw new PymanException("$command is required but not installed.");
}
}
$this->dependencies->check();
}

/**
Expand All @@ -51,7 +58,7 @@ private function checkPythonRequirements(): void
private function createPythonDirectory(): void
{
if (! is_dir($this->pythonDir) && ! mkdir($this->pythonDir, 0755, true)) {
throw new PymanException("Failed to create directory: {$this->pythonDir}");
throw new FolderCouldNotBeCreatedException("Failed to create directory: {$this->pythonDir}");
}
}

Expand All @@ -66,31 +73,17 @@ private function createVirtualEnvironment(): void
->run('python3 -m venv venv');

if (! $result->successful()) {
throw new PymanException('Failed to create virtual environment: '.$result->errorOutput());
throw new VenvManagementException('Failed to create virtual environment: '.$result->errorOutput());
}
}
}

/**
* @throws PymanException
*/
private function installDependencies(): void
private function installRequirements(): void
{
$requirementsPath = $this->pythonDir.'/requirements.txt';

if (! file_exists($requirementsPath)) {
throw new PymanException("requirements.txt not found in {$this->pythonDir}");
}

$venvPip = $this->pythonDir.'/venv/bin/pip';

$result = $this->makeProcess()->path($this->pythonDir)
->timeout(300)
->run([$venvPip, 'install', '-r', 'requirements.txt', '-q']);

if (! $result->successful()) {
throw new PymanException('Failed to install dependencies: '.$result->errorOutput());
}
$this->requirements->install();
}

private function makeProcess(): PendingProcess
Expand Down
66 changes: 66 additions & 0 deletions src/RequirementsManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Kauffinger\Pyman;

use Illuminate\Process\Factory;
use Kauffinger\Pyman\Exceptions\VenvManagementException;

class RequirementsManager
{
/**
* Holds list of dependencies that are required for the installation process.
*
* @param array<string> $requirements
*/
public function __construct(
private readonly Factory $processFactory,
private readonly string $pythonDir,
private array $requirements = []
) {}

public function add(string $requirement): void
{
$this->requirements[] = $requirement;
}

public function clear(): void
{
$this->requirements = [];
}

/**
* @return string[]
*/
public function get(): array
{
return $this->requirements;
}

/**
* Currently only works with the requirements.txt file, but will be extended to be able to use
* passed dependencies as well.
*
* @throws VenvManagementException
*/
public function install(): void
{
$requirementsPath = $this->pythonDir.'/requirements.txt';

if (! file_exists($requirementsPath)) {
throw new VenvManagementException("requirements.txt not found in {$this->pythonDir}");
}

$venvPip = $this->pythonDir.'/venv/bin/pip';

$result = $this->processFactory->newPendingProcess()
->path($this->pythonDir)
->timeout(300)
->run([$venvPip, 'install', '-r', 'requirements.txt', '-q']);

if (! $result->successful()) {
throw new VenvManagementException('Failed to install requirements: '.$result->errorOutput());
}
}
}
2 changes: 1 addition & 1 deletion tests/PythonEnvironmentManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@
$this->manager->setup();
test()->fail('Expected PymanException was not thrown');
} catch (PymanException $e) {
expect($e->getMessage())->toContain('Failed to install dependencies');
expect($e->getMessage())->toContain('Failed to install requirements');
}
});

0 comments on commit 6ffc08e

Please sign in to comment.