source: trunk/project-meta/project-meta @ 405

Last change on this file since 405 was 405, checked in by g7moreau, 5 years ago
  • Small error in dataset size
  • Property svn:executable set to *
File size: 21.9 KB
Line 
1#!/usr/bin/env perl
2#
3# 2018/01/17 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>
4#
5# apt-get install libyaml-syck-perl libtemplate-perl libarchive-zip-perl
6# apt-get install yamllint libyaml-shell-perl # check YAML files
7
8use strict;
9use warnings;
10use version; our $VERSION = version->declare('0.1.8');
11
12use File::Copy qw(copy);   
13use YAML::Syck;
14use Getopt::Long();
15use Cwd();
16use Template;
17use Archive::Zip qw(:ERROR_CODES :CONSTANTS);
18
19our $CFG_VERSION = 2;
20
21my ($verbose);
22Getopt::Long::GetOptions(
23   'verbose' => \$verbose,
24   );
25
26
27my %CMD_DB = (
28   'help'                  => \&cmd_help,
29   'version'               => \&cmd_version,
30   'check'                 => \&cmd_check,
31   'dap-publish'           => \&cmd_dap_publish,
32   'dap-unpublish'         => \&cmd_dap_unpublish,
33   'dataset-size'          => \&cmd_dataset_size,
34   'make-zip'              => \&cmd_make_zip,
35   'make-allfiles'         => \&cmd_make_allfiles,
36   'make-file-author'      => \&cmd_make_file_author,
37   'make-file-copyright'   => \&cmd_make_file_copyright,
38   'make-file-license'     => \&cmd_make_file_license,
39   'list-license'          => \&cmd_list_license,
40   'upgrade'               => \&cmd_upgrade,
41   );
42
43################################################################
44# main program
45################################################################
46
47my $cmd = shift @ARGV || 'help';
48if (defined $CMD_DB{$cmd}) {
49   $CMD_DB{$cmd}->(@ARGV);
50   }
51else {
52   print {*STDERR} "project-meta: command $cmd not found\n\n";
53   $CMD_DB{'help'}->();
54   exit 1;
55   }
56
57exit;
58
59################################################################
60# subroutine
61################################################################
62
63sub print_ok {
64   my ($key, $test) = @_;
65   
66   printf "%-35s : %s\n", $key, $test ? 'yes' : 'no';
67   }
68
69################################################################
70
71sub addfolder2list {
72   my ($folderdb, $folder) = @_;
73   
74   return if $folder !~ m{/};
75   
76   $folder =~ s{/[^/]+$}{};
77
78   $folderdb->{$folder}++;
79   return addfolder2list($folderdb, $folder);
80   }
81
82################################################################
83
84sub upgrade_version_1_to_2 {
85   my $meta = shift;
86
87   $meta->{'project'}{'identifier'} ||= {};
88   $meta->{'project'}{'identifier'}{'acronym'} = $meta->{'project'}{'acronym'};
89   delete $meta->{'project'}{'acronym'};
90
91   $meta->{'project'}{'creator'} = $meta->{'project'}{'authors'};
92   delete $meta->{'project'}{'authors'};
93
94   $meta->{'project'}{'description'} = $meta->{'project'}{'short-description'};
95   delete $meta->{'project'}{'short-description'};
96
97   $meta->{'project'}{'rights'} = $meta->{'public-dap'}{'data-license'};
98   delete $meta->{'public-dap'}{'data-license'};
99
100   $meta->{'project'}{'relation'} ||= [];
101   for my $doi (@{$meta->{'publication'}{'doi'}}) {
102      push @{$meta->{'project'}{'relation'}}, {doi => $doi};
103      }
104   delete $meta->{'publication'}{'doi'};
105
106   $meta->{'version'} = 2;
107   return $meta;
108   }
109
110################################################################
111
112sub load_metadata {
113   my $meta = YAML::Syck::LoadFile("PROJECT-META.yml");
114
115   my $initial_version = $meta->{'version'};
116   if ($initial_version < $CFG_VERSION) {
117      print "Warning: upgrade config file from version $initial_version to last version $CFG_VERSION\n";
118      my $upgrade = 'upgrade_version_' . ($CFG_VERSION - 1) . '_to_' . $CFG_VERSION;
119      &{$upgrade}($meta);
120      $initial_version = $CFG_VERSION;
121      }
122   elsif ($initial_version < $CFG_VERSION) {
123      die "Error: config file at future version $meta->{'version'}, program only at $CFG_VERSION\n"
124      }
125
126   return wantarray ? ($meta, $initial_version) : $meta;
127   }
128
129################################################################
130# command
131################################################################
132
133sub cmd_help {
134   print <<'END';
135project-meta - opendata project metafile manager
136
137 project-meta help
138 project-meta version
139 project-meta check
140 project-meta dap-publish
141 project-meta dap-unpublish
142 project-meta dataset-size
143 project-meta make-zip
144 project-meta make-allfiles
145 project-meta list-license
146 project-meta make-file-license
147 project-meta make-file-author
148 project-meta make-file-copyright
149 project-meta upgrade
150END
151   }
152
153################################################################
154
155sub cmd_version {
156   print "$VERSION\n";
157   }
158
159################################################################
160
161sub cmd_upgrade {
162   my ($meta, $initial_version) = load_metadata();
163
164   if ($initial_version < $meta->{'version'}) {
165      my $next_config = "PROJECT-META-v$meta->{'version'}.yml";
166      if (-e $next_config) {
167         die "Error: upgrade propose config file $next_config already exists\n";
168         }
169     
170      print "Warning: create new config file $next_config, please verify before using it\n";
171      YAML::Syck::SaveFile($next_config, $meta);
172      }
173   elsif ($initial_version == $CFG_VERSION) {
174      print "Warning: nothing to do, config file already at version $CFG_VERSION\n";
175      }
176   }
177
178################################################################
179
180sub cmd_check {
181   my $meta = load_metadata();
182
183   my $acronym     = $meta->{'project'}{'identifier'}{'acronym'};
184   my $current_dir = Cwd::getcwd();
185   my $dap_folder  = $meta->{'public-dap'}{'dap-folder'};
186
187   print_ok 'project/identifier/acronym',       $acronym =~ m{\d\d\w[\w\d_/]+};
188   print_ok 'public-dap/dap-folder',            $dap_folder ne '' and $dap_folder =~ m{^/};
189   print_ok 'dap-folder not match current_dir', $dap_folder !~ m{$current_dir};
190
191   #print YAML::Syck::Dump($meta);
192   }
193
194################################################################
195
196sub cmd_dap_publish {
197   my $meta = load_metadata();
198   my $current_dir = Cwd::getcwd();
199   my $acronym     = $meta->{'project'}{'identifier'}{'acronym'};
200   my $dap_folder  = $meta->{'public-dap'}{'dap-folder'};
201   my $data_set    = $meta->{'public-dap'}{'data-set'};
202
203   push @{$data_set}, 'AUTHORS.txt', 'COPYRIGHT.txt', 'LICENSE.txt';
204   {
205      # Remove doublon
206      my %seen = ();
207      @{$data_set} = grep { ! $seen{$_}++ } @{$data_set};
208      }
209
210   # Create a list of the folder
211   my %folders;
212   for my $dataset (@{$data_set}) {
213      addfolder2list(\%folders, $dataset);
214      }
215
216   print "chmod o+rX,o-w '$current_dir'\n";
217   print "mkdir -p '$dap_folder/$acronym'\n" if not -d "$dap_folder/$acronym";
218   for my $folder (sort keys %folders) {
219      print "chmod o+rX,o-w '$current_dir/$folder'\n";
220      print "mkdir '$dap_folder/$acronym/$folder'\n" if -d "$current_dir/$folder";
221      }
222
223   for my $dataset (@{$data_set}) {
224      if ($dataset =~ m{/}) {
225         # sub-folder case
226         my $folder = $dataset =~ s{/[^/]+$}{}r;
227         print "chmod -R o+rX,o-w '$current_dir/$dataset'\n";
228         print "ln --symbolic --target-directory '$dap_folder/$acronym/$folder/' '$current_dir/$dataset'\n";
229         }
230      else {
231         # Root case
232         print "ln --symbolic --target-directory '$dap_folder/$acronym/' '$current_dir/$dataset'\n";
233         }
234
235      }
236   print "chmod -R o+rX,o-w '$dap_folder/$acronym/'\n";
237   }
238
239################################################################
240
241sub cmd_dap_unpublish {
242   my $meta = load_metadata();
243   my $current_dir = Cwd::getcwd();
244   my $acronym     = $meta->{'project'}{'identifier'}{'acronym'};
245   my $dap_folder  = $meta->{'public-dap'}{'dap-folder'};
246
247   die "Error: DAP folder match current folder" if $dap_folder =~ m{$current_dir} or $current_dir =~ m{$dap_folder};
248
249   print "find '$dap_folder/$acronym/' -type l -o -type d -exec ls -l {} \+\n";
250   print "find '$dap_folder/$acronym/' -type l -delete\n";
251   print "find '$dap_folder/$acronym/' -type d -delete\n";
252   }
253
254################################################################
255
256sub cmd_dataset_size {
257   my $meta = load_metadata();
258   my $data_set = $meta->{'public-dap'}{'data-set'};
259   my $total;
260   for my $dataset (@{$data_set}) {
261      my $cmd = qx{du -sm $dataset};
262      chomp $cmd;
263      my ($size, $inode) = split /\s+/, $cmd;
264      $total += $size;
265      printf "%-7i %s\n", $size, $inode;
266      }
267   print "%-7i %s\n", $total, 'TOTAL';
268   }
269
270################################################################
271sub cmd_make_zip {
272   my $meta = load_metadata();
273   my $current_dir = Cwd::getcwd();
274   my $data_set    = $meta->{'public-dap'}{'data-set'};
275   my $acronym     = $meta->{'project'}{'identifier'}{'acronym'};
276
277   push @{$data_set}, 'AUTHORS.txt', 'COPYRIGHT.txt', 'LICENSE.txt';
278   {
279      # Remove doublon
280      my %seen = ();
281      @{$data_set} = grep { ! $seen{$_}++ } @{$data_set};
282      }
283
284   # Create a Zip file
285   my $zip = Archive::Zip->new();
286
287   for my $dataset (@{$data_set}) {
288      if (-d $dataset) {
289         # Folder case
290         $zip->addTree($dataset, "$acronym/$dataset");
291         }
292      elsif (-f $dataset) {
293         # File case
294         $zip->addFile($dataset, "$acronym/$dataset");
295         }
296      else {
297         # Strange case
298         print "Error: entry $dataset doesn't exists\n";
299         }
300      }
301
302   my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime time;
303   $year += 1900;
304   $mon++;
305   my $date = sprintf '%04i%02i%02i-%02i%02i', $year, $mon, $mday, $hour, $min;
306
307   # Save the Zip file
308   unless ($zip->writeToFileNamed("$current_dir/$acronym--$date.zip") == AZ_OK) {
309      die 'Error: zip write error';
310      }
311   }
312
313################################################################
314
315sub cmd_make_allfiles {
316   cmd_make_file_author();
317   cmd_make_file_license();
318   cmd_make_file_copyright();
319   }
320
321################################################################
322
323sub cmd_make_file_author {
324   my $meta = load_metadata();
325
326   my $current_dir = Cwd::getcwd();
327
328   my $acronym    = $meta->{'project'}{'identifier'}{'acronym'};
329   my $authors_list = $meta->{'project'}{'creator'};
330
331   if (-f "$current_dir/AUTHORS.txt") {
332      # Test for manual or automatically generated file
333      # Automatically generated file by project-meta
334      my $automatic;
335      open my $fh, '<', "$current_dir/AUTHORS.txt" or die $!;
336      for my $line (<$fh>) {
337         $line =~ m/Automatically generated .* project-meta/i and $automatic++;
338         }
339      close $fh;
340
341      if (not $automatic) {
342         print "Warning: AUTHORS.txt already exists\n";
343         return;
344         }
345
346      print "Warning: update AUTHORS.txt\n";
347      }
348
349   my $tt = Template->new(INCLUDE_PATH => '/usr/share/project-meta/template.d');
350   my $msg_format = '';
351   $tt->process('AUTHORS.tt',
352      {
353         acronym    => $acronym,
354         authorlist => $authors_list,
355      }, \$msg_format) || die $tt->error;
356
357   open my $fh,  '>', "$current_dir/AUTHORS.txt" or die $!;
358   print $fh "$msg_format\n\n";
359   close $fh;
360   }
361
362################################################################
363
364sub cmd_make_file_license {
365   my $meta = load_metadata();
366
367   my $current_dir = Cwd::getcwd();
368
369   if (-f "$current_dir/LICENSE.txt") {
370      print "Warning: LICENSE.txt already exists\n";
371      return;
372      }
373
374   my $license = $meta->{'project'}{'rights'};
375
376   if (not -f "/usr/share/project-meta/license.d/$license.txt") {
377      print "Error: license $license doesn't exists in project-meta database\n";
378      exit 1;
379      }
380
381   copy("/usr/share/project-meta/license.d/$license.txt", "$current_dir/LICENSE.txt")
382      or die "Error: license copy failed - $!";
383
384   print "Info: LICENSE.txt file create\n";
385   return;
386   }
387
388################################################################
389
390sub cmd_make_file_copyright {
391   my $meta = load_metadata();
392
393   my $current_dir = Cwd::getcwd();
394
395   if (-f "$current_dir/COPYRIGHT.txt") {
396      # Test for manual or automatically generated file
397      # Automatically generated file by project-meta
398      my $automatic;
399      open my $fh, '<', "$current_dir/COPYRIGHT.txt" or die $!;
400      for my $line (<$fh>) {
401         $line =~ m/Automatically generated .* project-meta/i and $automatic++;
402         }
403      close $fh;
404
405      if (not $automatic) {
406         print "Warning: COPYRIGHT.txt already exists\n";
407         return;
408         }
409
410      print "Warning: update COPYRIGHT.txt\n";
411      }
412   
413   my $tt = Template->new(
414      INCLUDE_PATH   => '/usr/share/project-meta/template.d',
415      POST_CHOMP     => 1, # Remove space and carriage return after %]
416      );
417   my $msg_format = '';
418   my $doi_first  = '';
419   if (exists $meta->{'project'}{'relation'}) {
420      for my $doi (@{$meta->{'project'}{'relation'}}) {
421         next if not exists $doi->{'doi'};
422         $doi_first = $doi->{'doi'};
423         last;
424         }
425      }
426   $tt->process('COPYRIGHT.tt',
427      {
428         title       => $meta->{'project'}{'title'},
429         acronym     => $meta->{'project'}{'identifier'}{'acronym'},
430         authorlist  => $meta->{'project'}{'creator'},
431         description => $meta->{'project'}{'description'},
432         license     => $meta->{'project'}{'rights'},
433         doi         => $doi_first,
434      }, \$msg_format) || die $tt->error;
435
436   open my $fh, '>', "$current_dir/COPYRIGHT.txt" or die $!;
437   print $fh "$msg_format\n\n";
438   close $fh;
439   }
440
441################################################################
442
443sub cmd_list_license {
444   opendir my $dh, '/usr/share/project-meta/license.d/' or die $!;
445   for my $license (readdir $dh) {
446      # Keep only file
447      next if not -f "/usr/share/project-meta/license.d/$license";
448     
449      # Keep only .txt file
450      next if not $license =~ m/\.txt$/;
451
452      $license =~ s/\.txt$//;
453      print "$license\n";
454      }
455   closedir $dh;
456   }
457
458################################################################
459# documentation
460################################################################
461
462__END__
463
464=head1 NAME
465
466project-meta - opendata project metafile manager
467
468
469=head1 USAGE
470
471 project-meta help
472 project-meta version
473 project-meta check
474 project-meta dap-publish
475 project-meta dap-unpublish
476 project-meta dataset-size
477 project-meta make-zip
478 project-meta list-license
479 project-meta make-file-license
480 project-meta make-file-author
481 project-meta make-file-copyright
482 project-meta upgrade
483
484
485=head1 DESCRIPTION
486
487Project-Meta is a small tool to maintain a set of open data files.
488In order to help you in this task, C<project-meta> command has a set of action
489to generated and maintain many files in your dataset.
490
491Everything is declare in the metafile F<PROJECT-META.yml>.
492This YAML file must exist in your root projet folder.
493See L</METAFILE SPECIFICATION>.
494
495
496=head1 COMMANDS
497
498Some command are defined in the source code but are not documented here.
499Theses could be not well defined, not finished, not well tested...
500You can read the source code and use them at your own risk
501(like for all the Project-Meta code).
502
503=head2 check
504
505 project-meta check
506
507Check your F<PROJECT-META.yml> has the good key.
508If your metafile is not a valid YAML file,
509you can use C<yamllint> or C<ysh> commands to check just it's format.
510
511=head2 dap-publish
512
513 project-meta dap-publish
514
515Publish data on an OpeNDAP server.
516Because data can be very large,
517This command just create UNIX soft links on the OpeNDAP folder to the real data.
518There is no copy.
519Files F<AUTHORS.txt>, F<LICENSE.txt> and F<COPYRIGHT.txt> are mandatory but could be generated (see below).
520The main keys use in the F<PROJECT-META.yml> are:
521
522=over
523
524=item * C<project/identifier/acronym>: the project short acronym, add to the OpeNDAP root folder
525
526=item * C<public-dap/dap-folder>: the OpeNDAP root folder
527
528=item * C<public-dap/data-set>: a list of files or folder to push
529
530=back
531
532Because this command could be dangerous, it does nothing!
533It print on terminal shell command to be done.
534You have to verify ouput before eval it.
535
536 project-meta dap-publish
537 project-meta dap-publish | bash
538
539=head2 dap-unpublish
540
541 project-meta dap-unpublish
542
543Unpublish data from the OpeNDAP server.
544In practice, it remove links in OpeNDAP folder for that projet.
545Because command C<rm> is always dangerous,
546we use here the command C<find> limited to folder and link.
547
548Please verify the returned values before excuted it with the C<-delete> option.
549
550=head2 dataset-size
551
552 project-meta dataset-size
553
554=head2 make-zip
555
556 project-meta make-zip
557
558Create a ZIP archive with the open data set.
559Files F<AUTHORS.txt>, F<LICENSE.txt> and F<COPYRIGHT.txt> are mandatory but could be generated (see below).
560The main keys use in the F<PROJECT-META.yml> are:
561
562=over
563
564=item * C<project/identifier/acronym>: the project short acronym, use as root folder
565
566=item * C<public-dap/data-set>: a list of files or folder to push
567
568=back
569
570=head2 make-allfiles
571
572 project-meta make-allfiles
573
574Generate or update all files: F<AUTHORS.txt>, F<COPYRIGHT.txt> and F<LICENSE.txt>.
575This command is just a shortcut for L</make-file-author>, L</make-file-copyright> and L</make-file-license>.
576
577
578=head2 list-license
579
580 project-meta list-license
581
582Give the list of all the open data licenses supported by the project-meta license database.
583At this time the possible licenses are:
584
585=over
586
587=item * L<community-data-license-agreement-permissive-v1.0|https://cdla.io/permissive-1-0/wp-content/uploads/sites/52/2017/10/CDLA-Permissive-v1.0.pdf>
588        (permissive - allow users to freely share and adapt)
589
590=item * L<community-data-license-agreement-sharing-v1.0|https://cdla.io/sharing-1-0/wp-content/uploads/sites/52/2017/10/CDLA-Sharing-v1.0.pdf>
591        (copyleft - allow users to freely share and adapt while maintaining this same freedom for others)
592
593=item * L<creative-common-attribution-v4.0|https://creativecommons.org/licenses/by/4.0/legalcode.txt>
594        (copyleft - allow users to freely share and adapt while maintaining this same freedom for others)
595
596=item * L<creative-common-zero-v1.0|https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt>
597        (like public domain)
598
599=item * L<licence-ouverte-v2.0|https://www.etalab.gouv.fr/wp-content/uploads/2017/04/ETALAB-Licence-Ouverte-v2.0.pdf>
600        (copyleft - opendata french goverment)
601
602=item * L<open-database-license-v1.0|https://opendatacommons.org/files/2018/02/odbl-10.txt>
603        (copyleft - allow users to freely share, modify, and use the database while maintaining this same freedom for others)
604
605=back
606
607Note that these licenses are dedicated to open data.
608Please do not use an open license that would have been thought for source code or documentation and not for open data.
609Here are some links about open data licence context:
610
611=over
612
613=item * A good article about Community Data License Agreement and Open Data Licence in general
614   L<Licenses for data|https://lwn.net/Articles/753648/> written on 9 May 2018.
615
616=item * A french page about French Public Open Data licence
617   L<https://www.etalab.gouv.fr/licence-ouverte-open-licence>.
618
619=back
620
621=head2 make-file-license
622
623 project-meta make-file-license
624
625Copy the license file from the project-meta license database at the current folder
626with the file name: F<LICENSE.txt>.
627
628The license is defined in the F<PROJECT-META.yml> specification under the key C<public-dap/data-license>.
629The list of possible license is given with the command L</list-license>.
630
631=head2 make-file-author
632
633 project-meta make-file-author
634
635Create or update the F<AUTHORS.txt> file at the current folder.
636Authors data are extracted from the C<PROJECT-META.yml> file.
637
638=head2 make-file-copyright
639
640 project-meta make-file-copyright
641
642Create or update the F<COPYRIGHT.txt> file at the current folder.
643Authors, license and copyright data are extracted from the C<PROJECT-META.yml> file.
644
645=head2 upgrade
646
647 project-meta upgrade
648
649Upgrade config file to last version.
650Create a file F<PROJECT-META-vVERSION.yml> in the current directory if it's not exists, error otherwise.
651Please maually verify this autogenerated config file before rename and using it.
652
653
654=head1 METAFILE SPECIFICATION
655
656Each project must have an open data metafile describing the project : C<PROJECT-META.yml>.
657The file is in YAML format because this is a human-readable text file style.
658Other formats could have been Plain XML, RDF, JSON... but they are much less readable.
659
660You can find in the project-meta software a
661L<PROJECT-META.sample.yml|http://servforge.legi.grenoble-inp.fr/pub/soft-trokata/project-meta/PROJECT-META.sample.yml> example.
662This one is actually the master reference specification!
663
664Some interresting papers or links about Open Meta Data Schema:
665
666=over
667
668=item * L<Metadata for the open data portals|http://devinit.org/wp-content/uploads/2018/01/Metadata-for-open-data-portals.pdf>
669        writen in december 2016.
670
671=item * L<Project Open Data Metadata Schema v1.1|https://project-open-data.cio.gov/v1.1/schema/> from US governement
672        based on L<DCAT|http://www.w3.org/TR/vocab-dcat/>.
673
674=item * L<Metadata Standards|http://knowhow.opendatamonitor.eu/odresearch/metadata-standards/>
675        from OpenDataMonitor.
676
677=item * L<G8 Metadata Mapping|https://github.com/project-open-data/G8_Metadata_Mapping/blob/master/index.md>
678        mapping between the metadata on datasets published by G8 Members through their open data portals.
679
680=back
681
682
683=head1 KNOWN BUGS
684
685 - not really check keys and tags before doing action!
686
687
688=head1 SEE ALSO
689
690yamllint(1), ysh(1), YAML, Archive::Zip
691
692In Debian GNU/Linux distribution, packages for C<yamllint> and C<ysh> are:
693
694=over
695
696=item * C<yamllint> - Linter for YAML files (Python)
697
698=item * C<libyaml-shell-perl> - YAML test shell (Perl)
699
700=back
701
702
703Own project ressources:
704
705=over
706
707=item * L<Web site|http://servforge.legi.grenoble-inp.fr/projects/soft-trokata/wiki/SoftWare/ProjectMeta>
708
709=item * L<Online Manual|http://servforge.legi.grenoble-inp.fr/pub/soft-trokata/project-meta/project-meta.html>
710
711=item * L<SVN repository|http://servforge.legi.grenoble-inp.fr/svn/soft-trokata/trunk/project-meta>
712
713=back
714
715
716=head1 AUTHOR
717
718Written by Gabriel Moreau, LEGI UMR5519, CNRS, Grenoble - France
719
720
721=head1 SPECIAL THANKS
722
723The list of people below did not directly contribute to project-meta's source code
724but provided me with some data, returned bugs
725or helped me in another task like having new ideas, specifications...
726Maybe I forgot your contribution in recent years,
727please forgive me in advance and send me an e-mail to correct this.
728
729Joel Sommeria, Julien Chauchat, Cyrille Bonamy, Antoine Mathieu.
730
731
732=head1 LICENSE AND COPYRIGHT
733
734License GNU GPL version 2 or later and Perl equivalent
735
736Copyright (C) 2017-2018 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>.
Note: See TracBrowser for help on using the repository browser.