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

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