Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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