Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Python predictions processor
19
 *
20
 * @package   mlbackend_python
21
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace mlbackend_python;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
1441 ariadna 29
global $CFG;
30
require_once($CFG->dirroot . '/analytics/tests/classes/mlbackend_helper_trait.php');
31
 
32
use core_analytics\tests\mlbackend_helper_trait;
33
 
1 efrain 34
/**
35
 * Python predictions processor.
36
 *
37
 * @package   mlbackend_python
38
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
39
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
 */
41
class processor implements  \core_analytics\classifier, \core_analytics\regressor, \core_analytics\packable {
42
 
1441 ariadna 43
    use mlbackend_helper_trait;
44
 
1 efrain 45
    /**
46
     * The required version of the python package that performs all calculations.
47
     */
48
    const REQUIRED_PIP_PACKAGE_VERSION = '3.0.5';
49
 
50
    /**
51
     * The python package is installed in a server.
52
     * @var bool
53
     */
54
    protected $useserver;
55
 
56
    /**
57
     * The path to the Python bin.
58
     *
59
     * @var string
60
     */
61
    protected $pathtopython;
62
 
63
    /**
64
     * Remote server host
65
     * @var string
66
     */
67
    protected $host;
68
 
69
    /**
70
     * Remote server port
71
     * @var int
72
     */
73
    protected $port;
74
 
75
    /**
76
     * Whether to use http or https.
77
     * @var bool
78
     */
79
    protected $secure;
80
 
81
    /**
82
     * Server username.
83
     * @var string
84
     */
85
    protected $username;
86
 
87
    /**
88
     * Server password for $this->username.
89
     * @var string
90
     */
91
    protected $password;
92
 
93
    /**
94
     * The constructor.
95
     *
96
     */
97
    public function __construct() {
98
        global $CFG;
99
 
1441 ariadna 100
        if ((defined('BEHAT_SITE_RUNNING') || (defined('PHPUNIT_TEST') && PHPUNIT_TEST)) &&
101
                self::is_mlbackend_python_configured()) {
102
            $this->useserver = true;
103
            $this->host = TEST_MLBACKEND_PYTHON_HOST;
104
            $this->port = TEST_MLBACKEND_PYTHON_PORT;
105
            $this->secure = false;
106
            $this->username = TEST_MLBACKEND_PYTHON_USERNAME;
107
            $this->password = TEST_MLBACKEND_PYTHON_PASSWORD;
108
        } else {
109
            $config = get_config('mlbackend_python');
1 efrain 110
 
1441 ariadna 111
            $this->useserver = !empty($config->useserver);
1 efrain 112
 
1441 ariadna 113
            if (!$this->useserver) {
114
                // Set the python location if there is a value.
115
                if (!empty($CFG->pathtopython)) {
116
                    $this->pathtopython = $CFG->pathtopython;
117
                }
118
            } else {
119
                $this->host = $config->host ?? '';
120
                $this->port = $config->port ?? '';
121
                $this->secure = $config->secure ?? false;
122
                $this->username = $config->username ?? '';
123
                $this->password = $config->password ?? '';
1 efrain 124
            }
125
        }
126
    }
127
 
128
    /**
129
     * Is the plugin ready to be used?.
130
     *
131
     * @return bool|string Returns true on success, a string detailing the error otherwise
132
     */
133
    public function is_ready() {
134
 
135
        if (!$this->useserver) {
136
            return $this->is_webserver_ready();
137
        } else {
138
            return $this->is_python_server_ready();
139
        }
140
    }
141
 
142
    /**
143
     * Checks if the python package is available in the web server executing this script.
144
     *
145
     * @return bool|string Returns true on success, a string detailing the error otherwise
146
     */
147
    protected function is_webserver_ready() {
148
        if (empty($this->pathtopython)) {
149
            $settingurl = new \moodle_url('/admin/settings.php', array('section' => 'systempaths'));
150
            return get_string('pythonpathnotdefined', 'mlbackend_python', $settingurl->out());
151
        }
152
 
153
        // Check the installed pip package version.
154
        $cmd = "{$this->pathtopython} -m moodlemlbackend.version";
155
 
156
        $output = null;
157
        $exitcode = null;
158
        // Execute it sending the standard error to $output.
159
        $result = exec($cmd . ' 2>&1', $output, $exitcode);
160
 
161
        if ($exitcode != 0) {
162
            return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
163
        }
164
 
165
        $vercheck = self::check_pip_package_version($result);
166
        return $this->version_check_return($result, $vercheck);
167
    }
168
 
169
    /**
170
     * Checks if the server can be accessed.
171
     *
172
     * @return bool|string True or an error string.
173
     */
174
    protected function is_python_server_ready() {
175
 
176
        if (empty($this->host) || empty($this->port) || empty($this->username) || empty($this->password)) {
177
            return get_string('errornoconfigdata', 'mlbackend_python');
178
        }
179
 
180
        // Connection is allowed to use 'localhost' and other potentially blocked hosts/ports.
181
        $curl = new \curl(['ignoresecurity' => true]);
182
        $responsebody = $curl->get($this->get_server_url('version')->out(false));
183
        if ($curl->info['http_code'] !== 200) {
184
            return get_string('errorserver', 'mlbackend_python', $this->server_error_str($curl->info['http_code'], $responsebody));
185
        }
186
 
187
        $vercheck = self::check_pip_package_version($responsebody);
188
        return $this->version_check_return($responsebody, $vercheck);
189
 
190
    }
191
 
192
    /**
193
     * Delete the model version output directory.
194
     *
195
     * @throws \moodle_exception
196
     * @param string $uniqueid
197
     * @param string $modelversionoutputdir
198
     * @return null
199
     */
200
    public function clear_model($uniqueid, $modelversionoutputdir) {
201
        if (!$this->useserver) {
202
            remove_dir($modelversionoutputdir);
203
        } else {
204
            // Use the server.
205
 
206
            $url = $this->get_server_url('deletemodel');
207
            list($responsebody, $httpcode) = $this->server_request($url, 'post', ['uniqueid' => $uniqueid]);
208
        }
209
    }
210
 
211
    /**
212
     * Delete the model output directory.
213
     *
214
     * @throws \moodle_exception
215
     * @param string $modeloutputdir
216
     * @param string $uniqueid
217
     * @return null
218
     */
219
    public function delete_output_dir($modeloutputdir, $uniqueid) {
220
        if (!$this->useserver) {
221
            remove_dir($modeloutputdir);
222
        } else {
223
 
224
            $url = $this->get_server_url('deletemodel');
225
            list($responsebody, $httpcode) = $this->server_request($url, 'post', ['uniqueid' => $uniqueid]);
226
        }
227
    }
228
 
229
    /**
230
     * Trains a machine learning algorithm with the provided dataset.
231
     *
232
     * @param string $uniqueid
233
     * @param \stored_file $dataset
234
     * @param string $outputdir
235
     * @return \stdClass
236
     */
237
    public function train_classification($uniqueid, \stored_file $dataset, $outputdir) {
238
 
239
        if (!$this->useserver) {
240
            // Use the local file system.
241
 
242
            list($result, $exitcode) = $this->exec_command('training', [$uniqueid, $outputdir,
243
                $this->get_file_path($dataset)], 'errornopredictresults');
244
 
245
        } else {
246
            // Use the server.
247
 
248
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
249
                'dataset' => $dataset];
250
 
251
            $url = $this->get_server_url('training');
252
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
253
        }
254
 
255
        if (!$resultobj = json_decode($result)) {
256
            throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
257
        }
258
 
259
        if ($resultobj->status != 0) {
260
            $resultobj = $this->format_error_info($resultobj);
261
        }
262
 
263
        return $resultobj;
264
    }
265
 
266
    /**
267
     * Classifies the provided dataset samples.
268
     *
269
     * @param string $uniqueid
270
     * @param \stored_file $dataset
271
     * @param string $outputdir
272
     * @return \stdClass
273
     */
274
    public function classify($uniqueid, \stored_file $dataset, $outputdir) {
275
 
276
        if (!$this->useserver) {
277
            // Use the local file system.
278
 
279
            list($result, $exitcode) = $this->exec_command('prediction', [$uniqueid, $outputdir,
280
                $this->get_file_path($dataset)], 'errornopredictresults');
281
 
282
        } else {
283
            // Use the server.
284
 
285
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
286
                'dataset' => $dataset];
287
 
288
            $url = $this->get_server_url('prediction');
289
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
290
        }
291
 
292
        if (!$resultobj = json_decode($result)) {
293
            throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
294
        }
295
 
296
        if ($resultobj->status != 0) {
297
            $resultobj = $this->format_error_info($resultobj);
298
        }
299
 
300
        return $resultobj;
301
    }
302
 
303
    /**
304
     * Evaluates this processor classification model using the provided supervised learning dataset.
305
     *
306
     * @param string $uniqueid
307
     * @param float $maxdeviation
308
     * @param int $niterations
309
     * @param \stored_file $dataset
310
     * @param string $outputdir
311
     * @param  string $trainedmodeldir
312
     * @return \stdClass
313
     */
314
    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
315
            $outputdir, $trainedmodeldir) {
316
        global $CFG;
317
 
318
        if (!$this->useserver) {
319
            // Use the local file system.
320
 
321
            $datasetpath = $this->get_file_path($dataset);
322
 
323
            $params = [$uniqueid, $outputdir, $datasetpath, \core_analytics\model::MIN_SCORE,
324
                $maxdeviation, $niterations];
325
 
326
            if ($trainedmodeldir) {
327
                $params[] = $trainedmodeldir;
328
            }
329
 
330
            list($result, $exitcode) = $this->exec_command('evaluation', $params, 'errornopredictresults');
331
            if (!$resultobj = json_decode($result)) {
332
                throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
333
            }
334
 
335
        } else {
336
            // Use the server.
337
 
338
            $requestparams = ['uniqueid' => $uniqueid, 'minscore' => \core_analytics\model::MIN_SCORE,
339
                'maxdeviation' => $maxdeviation, 'niterations' => $niterations,
340
                'dirhash' => $this->hash_dir($outputdir), 'dataset' => $dataset];
341
 
342
            if ($trainedmodeldir) {
343
                $requestparams['trainedmodeldirhash'] = $this->hash_dir($trainedmodeldir);
344
            }
345
 
346
            $url = $this->get_server_url('evaluation');
347
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
348
 
349
            if (!$resultobj = json_decode($result)) {
350
                throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
351
            }
352
 
353
            // We need an extra request to get the resources generated during the evaluation process.
354
 
355
            // Directory to temporarly store the evaluation log zip returned by the server.
356
            $evaluationtmpdir = make_request_directory();
357
            $evaluationzippath = $evaluationtmpdir . DIRECTORY_SEPARATOR . 'evaluationlog.zip';
358
 
359
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
360
            'runid' => $resultobj->runid];
361
 
362
            $url = $this->get_server_url('evaluationlog');
363
            list($result, $httpcode) = $this->server_request($url, 'download_one', $requestparams,
364
                ['filepath' => $evaluationzippath]);
365
 
366
            $rundir = $outputdir . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . $resultobj->runid;
367
            if (!mkdir($rundir, $CFG->directorypermissions, true)) {
368
                throw new \moodle_exception('errorexportmodelresult', 'analytics');
369
            }
370
 
371
            $zip = new \zip_packer();
372
            $success = $zip->extract_to_pathname($evaluationzippath, $rundir, null, null, true);
373
            if (!$success) {
374
                $a = 'The evaluation files can not be exported to ' . $rundir;
375
                throw new \moodle_exception('errorpredictionsprocessor', 'analytics', '', $a);
376
            }
377
 
378
            $resultobj->dir = $rundir;
379
        }
380
 
381
        $resultobj = $this->add_extra_result_info($resultobj);
382
 
383
        return $resultobj;
384
    }
385
 
386
    /**
387
     * Exports the machine learning model.
388
     *
389
     * @throws \moodle_exception
390
     * @param  string $uniqueid  The model unique id
391
     * @param  string $modeldir  The directory that contains the trained model.
392
     * @return string            The path to the directory that contains the exported model.
393
     */
394
    public function export(string $uniqueid, string $modeldir): string {
395
 
396
        $exporttmpdir = make_request_directory();
397
 
398
        if (!$this->useserver) {
399
            // Use the local file system.
400
 
401
            // We include an exporttmpdir as we want to be sure that the file is not deleted after the
402
            // python process finishes.
403
            list($exportdir, $exitcode) = $this->exec_command('export', [$uniqueid, $modeldir, $exporttmpdir],
404
                'errorexportmodelresult');
405
 
406
            if ($exitcode != 0) {
407
                throw new \moodle_exception('errorexportmodelresult', 'analytics');
408
            }
409
 
410
        } else {
411
            // Use the server.
412
 
413
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($modeldir)];
414
 
415
            $exportzippath = $exporttmpdir . DIRECTORY_SEPARATOR . 'export.zip';
416
            $url = $this->get_server_url('export');
417
            list($result, $httpcode) = $this->server_request($url, 'download_one', $requestparams,
418
                ['filepath' => $exportzippath]);
419
 
420
            $exportdir = make_request_directory();
421
            $zip = new \zip_packer();
422
            $success = $zip->extract_to_pathname($exportzippath, $exportdir, null, null, true);
423
            if (!$success) {
424
                throw new \moodle_exception('errorexportmodelresult', 'analytics');
425
            }
426
        }
427
 
428
        return $exportdir;
429
    }
430
 
431
    /**
432
     * Imports the provided machine learning model.
433
     *
434
     * @param  string $uniqueid The model unique id
435
     * @param  string $modeldir  The directory that will contain the trained model.
436
     * @param  string $importdir The directory that contains the files to import.
437
     * @return bool Success
438
     */
439
    public function import(string $uniqueid, string $modeldir, string $importdir): bool {
440
 
441
        if (!$this->useserver) {
442
            // Use the local file system.
443
 
444
            list($result, $exitcode) = $this->exec_command('import', [$uniqueid, $modeldir, $importdir],
445
                'errorimportmodelresult');
446
 
447
            if ($exitcode != 0) {
448
                throw new \moodle_exception('errorimportmodelresult', 'analytics');
449
            }
450
 
451
        } else {
452
            // Use the server.
453
 
454
            // Zip the $importdir to send a single file.
455
            $importzipfile = $this->zip_dir($importdir);
456
            if (!$importzipfile) {
457
                // There was an error zipping the directory.
458
                throw new \moodle_exception('errorimportmodelresult', 'analytics');
459
            }
460
 
461
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($modeldir),
462
                'importzip' => curl_file_create($importzipfile, null, 'import.zip')];
463
            $url = $this->get_server_url('import');
464
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
465
        }
466
 
467
        return (bool)$result;
468
    }
469
 
470
    /**
471
     * Train this processor regression model using the provided supervised learning dataset.
472
     *
473
     * @throws new \coding_exception
474
     * @param string $uniqueid
475
     * @param \stored_file $dataset
476
     * @param string $outputdir
477
     * @return \stdClass
478
     */
479
    public function train_regression($uniqueid, \stored_file $dataset, $outputdir) {
480
        throw new \coding_exception('This predictor does not support regression yet.');
481
    }
482
 
483
    /**
484
     * Estimates linear values for the provided dataset samples.
485
     *
486
     * @throws new \coding_exception
487
     * @param string $uniqueid
488
     * @param \stored_file $dataset
489
     * @param mixed $outputdir
490
     * @return void
491
     */
492
    public function estimate($uniqueid, \stored_file $dataset, $outputdir) {
493
        throw new \coding_exception('This predictor does not support regression yet.');
494
    }
495
 
496
    /**
497
     * Evaluates this processor regression model using the provided supervised learning dataset.
498
     *
499
     * @throws new \coding_exception
500
     * @param string $uniqueid
501
     * @param float $maxdeviation
502
     * @param int $niterations
503
     * @param \stored_file $dataset
504
     * @param string $outputdir
505
     * @param  string $trainedmodeldir
506
     * @return \stdClass
507
     */
508
    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
509
            $outputdir, $trainedmodeldir) {
510
        throw new \coding_exception('This predictor does not support regression yet.');
511
    }
512
 
513
    /**
514
     * Returns the path to the dataset file.
515
     *
516
     * @param \stored_file $file
517
     * @return string
518
     */
519
    protected function get_file_path(\stored_file $file) {
520
        // From moodle filesystem to the local file system.
521
        // This is not ideal, but there is no read access to moodle filesystem files.
522
        return $file->copy_content_to_temp('core_analytics');
523
    }
524
 
525
    /**
526
     * Check that the given package version can be used and return the error status.
527
     *
528
     * When evaluating the version, we assume the sematic versioning scheme as described at
529
     * https://semver.org/.
530
     *
531
     * @param string $actual The actual Python package version
532
     * @param string $required The required version of the package
533
     * @return int -1 = actual version is too low, 1 = actual version too high, 0 = actual version is ok
534
     */
535
    public static function check_pip_package_version($actual, $required = self::REQUIRED_PIP_PACKAGE_VERSION) {
536
 
537
        if (empty($actual)) {
538
            return -1;
539
        }
540
 
541
        if (version_compare($actual, $required, '<')) {
542
            return -1;
543
        }
544
 
545
        $parts = explode('.', $required);
546
        $requiredapiver = reset($parts);
547
 
548
        $parts = explode('.', $actual);
549
        $actualapiver = reset($parts);
550
 
551
        if ($requiredapiver > 0 || $actualapiver > 1) {
552
            if (version_compare($actual, $requiredapiver + 1, '>=')) {
553
                return 1;
554
            }
555
        }
556
 
557
        return 0;
558
    }
559
 
560
    /**
561
     * Executes the specified module.
562
     *
563
     * @param  string $modulename
564
     * @param  array  $params
565
     * @param  string $errorlangstr
566
     * @return array [0] is the result body and [1] the exit code.
567
     */
568
    protected function exec_command(string $modulename, array $params, string $errorlangstr) {
569
 
570
        $cmd = $this->pathtopython . ' -m moodlemlbackend.' . $modulename . ' ';
571
        foreach ($params as $param) {
572
            $cmd .= escapeshellarg($param) . ' ';
573
        }
574
 
575
        if (!PHPUNIT_TEST && CLI_SCRIPT) {
576
            debugging($cmd, DEBUG_DEVELOPER);
577
        }
578
 
579
        $output = null;
580
        $exitcode = null;
581
        $result = exec($cmd, $output, $exitcode);
582
 
583
        if (!$result) {
584
            throw new \moodle_exception($errorlangstr, 'analytics');
585
        }
586
 
587
        return [$result, $exitcode];
588
    }
589
 
590
    /**
591
     * Formats the errors and info in a single info string.
592
     *
593
     * @param  \stdClass $resultobj
594
     * @return \stdClass
595
     */
596
    private function format_error_info(\stdClass $resultobj) {
597
        if (!empty($resultobj->errors)) {
598
            $errors = $resultobj->errors;
599
            if (is_array($errors)) {
600
                $errors = implode(', ', $errors);
601
            }
602
        } else if (!empty($resultobj->info)) {
603
            // Show info if no errors are returned.
604
            $errors = $resultobj->info;
605
            if (is_array($errors)) {
606
                $errors = implode(', ', $errors);
607
            }
608
        }
609
        $resultobj->info = array(get_string('errorpredictionsprocessor', 'analytics', $errors));
610
 
611
        return $resultobj;
612
    }
613
 
614
    /**
615
     * Returns the url to the python ML server.
616
     *
617
     * @param  string|null $path
618
     * @return \moodle_url
619
     */
620
    private function get_server_url(?string $path = null) {
621
        $protocol = !empty($this->secure) ? 'https' : 'http';
622
        $url = $protocol . '://' . rtrim($this->host, '/');
623
        if (!empty($this->port)) {
624
            $url .= ':' . $this->port;
625
        }
626
 
627
        if ($path) {
628
            $url .= '/' . $path;
629
        }
630
 
631
        return new \moodle_url($url);
632
    }
633
 
634
    /**
635
     * Sends a request to the python ML server.
636
     *
637
     * @param  \moodle_url      $url            The requested url in the python ML server
638
     * @param  string           $method         The curl method to use
639
     * @param  array            $requestparams  Curl request params
640
     * @param  array|null       $options        Curl request options
641
     * @return array                            [0] for the response body and [1] for the http code
642
     */
643
    protected function server_request($url, string $method, array $requestparams, ?array $options = null) {
644
 
645
        if ($method !== 'post' && $method !== 'get' && $method !== 'download_one') {
646
            throw new \coding_exception('Incorrect request method provided. Only "get", "post" and "download_one"
647
                actions are available.');
648
        }
649
 
650
        // Connection is allowed to use 'localhost' and other potentially blocked hosts/ports.
651
        $curl = new \curl(['ignoresecurity' => true]);
652
 
653
        $authorization = $this->username . ':' . $this->password;
654
        $curl->setHeader('Authorization: Basic ' . base64_encode($authorization));
655
 
656
        $responsebody = $curl->{$method}($url, $requestparams, $options);
657
 
658
        if ($curl->info['http_code'] !== 200) {
659
            throw new \moodle_exception('errorserver', 'mlbackend_python', '',
660
                $this->server_error_str($curl->info['http_code'], $responsebody));
661
        }
662
 
663
        return [$responsebody, $curl->info['http_code']];
664
    }
665
 
666
    /**
667
     * Adds extra information to results info.
668
     *
669
     * @param  \stdClass $resultobj
670
     * @return \stdClass
671
     */
672
    protected function add_extra_result_info(\stdClass $resultobj): \stdClass {
673
 
674
        if (!empty($resultobj->dir)) {
675
            $dir = $resultobj->dir . DIRECTORY_SEPARATOR . 'tensor';
676
            $resultobj->info[] = get_string('tensorboardinfo', 'mlbackend_python', $dir);
677
        }
678
        return $resultobj;
679
    }
680
 
681
    /**
682
     * Returns the proper return value for the version checking.
683
     *
684
     * @param  string $actual   Actual moodlemlbackend version
685
     * @param  int    $vercheck Version checking result
686
     * @return true|string      Returns true on success, a string detailing the error otherwise
687
     */
688
    private function version_check_return($actual, $vercheck) {
689
 
690
        if ($vercheck === 0) {
691
            return true;
692
        }
693
 
694
        if ($actual) {
695
            $a = [
696
                'installed' => $actual,
697
                'required' => self::REQUIRED_PIP_PACKAGE_VERSION,
698
            ];
699
 
700
            if ($vercheck < 0) {
701
                return get_string('packageinstalledshouldbe', 'mlbackend_python', $a);
702
 
703
            } else if ($vercheck > 0) {
704
                return get_string('packageinstalledtoohigh', 'mlbackend_python', $a);
705
            }
706
        }
707
 
708
        if (!$this->useserver) {
709
            $cmd = "{$this->pathtopython} -m moodlemlbackend.version";
710
        } else {
711
            // We can't not know which is the python bin in the python ML server, the most likely
712
            // value is 'python'.
713
            $cmd = "python -m moodlemlbackend.version";
714
        }
715
        return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
716
    }
717
 
718
    /**
719
     * Hashes the provided dir as a string.
720
     *
721
     * @param  string $dir Directory path
722
     * @return string Hash
723
     */
724
    private function hash_dir(string $dir) {
725
        return md5($dir);
726
    }
727
 
728
    /**
729
     * Zips the provided directory.
730
     *
731
     * @param  string $dir Directory path
732
     * @return string The zip filename
733
     */
734
    private function zip_dir(string $dir) {
735
 
736
        $ziptmpdir = make_request_directory();
737
        $ziptmpfile = $ziptmpdir . DIRECTORY_SEPARATOR . 'mlbackend.zip';
738
 
739
        $files = get_directory_list($dir);
740
        $zipfiles = [];
741
        foreach ($files as $file) {
742
            $fullpath = $dir . DIRECTORY_SEPARATOR . $file;
743
            // Use the relative path to the file as the path in the zip.
744
            $zipfiles[$file] = $fullpath;
745
        }
746
 
747
        $zip = new \zip_packer();
748
        if (!$zip->archive_to_pathname($zipfiles, $ziptmpfile)) {
749
            return false;
750
        }
751
 
752
        return $ziptmpfile;
753
    }
754
 
755
    /**
756
     * Error string for httpcode !== 200
757
     *
758
     * @param int       $httpstatuscode The HTTP status code
759
     * @param string    $responsebody   The body of the response
760
     */
761
    private function server_error_str(int $httpstatuscode, string $responsebody): string {
762
        return 'HTTP status code ' . $httpstatuscode . ': ' . $responsebody;
763
    }
764
}