comparison roundup/configuration.py @ 6966:8733aa2a8e40

flake8 fixes.
author John Rouillard <rouilj@ieee.org>
date Tue, 13 Sep 2022 16:35:32 -0400
parents 9ff091537f43
children 570abc4c6548
comparison
equal deleted inserted replaced
6965:98d72d4bb489 6966:8733aa2a8e40
31 else: 31 else:
32 import ConfigParser as configparser # Python 2 32 import ConfigParser as configparser # Python 2
33 33
34 from roundup.exceptions import RoundupException 34 from roundup.exceptions import RoundupException
35 35
36 ### Exceptions 36 # Exceptions
37 37
38 38
39 class ConfigurationError(RoundupException): 39 class ConfigurationError(RoundupException):
40 pass 40 pass
41 41
117 117
118 118
119 def create_token(size=32): 119 def create_token(size=32):
120 return b2s(binascii.b2a_base64(random_.token_bytes(size)).strip()) 120 return b2s(binascii.b2a_base64(random_.token_bytes(size)).strip())
121 121
122 ### Option classes 122 # Option classes
123 123
124 124
125 class Option: 125 class Option:
126 126
127 """Single configuration option. 127 """Single configuration option.
432 432
433 # FIXME this is the result of running: 433 # FIXME this is the result of running:
434 # SELECT cfgname FROM pg_ts_config; 434 # SELECT cfgname FROM pg_ts_config;
435 # on a postgresql 14.1 server. 435 # on a postgresql 14.1 server.
436 # So the best we can do is hardcode this. 436 # So the best we can do is hardcode this.
437 valid_langs = [ "simple", 437 valid_langs = ["simple",
438 "custom1", 438 "custom1",
439 "custom2", 439 "custom2",
440 "custom3", 440 "custom3",
441 "custom4", 441 "custom4",
442 "custom5", 442 "custom5",
443 "arabic", 443 "arabic",
444 "armenian", 444 "armenian",
445 "basque", 445 "basque",
446 "catalan", 446 "catalan",
447 "danish", 447 "danish",
448 "dutch", 448 "dutch",
449 "english", 449 "english",
450 "finnish", 450 "finnish",
451 "french", 451 "french",
452 "german", 452 "german",
453 "greek", 453 "greek",
454 "hindi", 454 "hindi",
455 "hungarian", 455 "hungarian",
456 "indonesian", 456 "indonesian",
457 "irish", 457 "irish",
458 "italian", 458 "italian",
459 "lithuanian", 459 "lithuanian",
460 "nepali", 460 "nepali",
461 "norwegian", 461 "norwegian",
462 "portuguese", 462 "portuguese",
463 "romanian", 463 "romanian",
464 "russian", 464 "russian",
465 "serbian", 465 "serbian",
466 "spanish", 466 "spanish",
467 "swedish", 467 "swedish",
468 "tamil", 468 "tamil",
469 "turkish", 469 "turkish",
470 "yiddish" ] 470 "yiddish"]
471 471
472 def str2value(self, value): 472 def str2value(self, value):
473 _val = value.lower() 473 _val = value.lower()
474 if _val in self.allowed: 474 if _val in self.allowed:
475 return _val 475 return _val
476 raise OptionValueError(self, value, self.class_description) 476 raise OptionValueError(self, value, self.class_description)
493 lang_avail = b2s(xapian.Stem.get_available_languages()) 493 lang_avail = b2s(xapian.Stem.get_available_languages())
494 languages = textwrap.fill(_("Valid languages: ") + 494 languages = textwrap.fill(_("Valid languages: ") +
495 lang_avail, 75, 495 lang_avail, 75,
496 subsequent_indent=" ") 496 subsequent_indent=" ")
497 raise OptionValueError(options["INDEXER_LANGUAGE"], 497 raise OptionValueError(options["INDEXER_LANGUAGE"],
498 lang, languages) 498 lang, languages)
499 499
500 if self._value == "native-fts": 500 if self._value == "native-fts":
501 lang = options["INDEXER_LANGUAGE"]._value 501 lang = options["INDEXER_LANGUAGE"]._value
502 if lang not in self.valid_langs: 502 if lang not in self.valid_langs:
503 import textwrap 503 import textwrap
504 languages = textwrap.fill(_("Expected languages: ") + 504 languages = textwrap.fill(_("Expected languages: ") +
505 " ".join(self.valid_langs), 75, 505 " ".join(self.valid_langs), 75,
506 subsequent_indent=" ") 506 subsequent_indent=" ")
507 raise OptionValueError(options["INDEXER_LANGUAGE"], 507 raise OptionValueError(options["INDEXER_LANGUAGE"],
508 lang, languages) 508 lang, languages)
509
509 510
510 class MailAddressOption(Option): 511 class MailAddressOption(Option):
511 512
512 """Email address 513 """Email address
513 514
538 _val = Option.get(self) 539 _val = Option.get(self)
539 if _val and not os.path.isabs(_val): 540 if _val and not os.path.isabs(_val):
540 _val = os.path.join(self.config["HOME"], _val) 541 _val = os.path.join(self.config["HOME"], _val)
541 return _val 542 return _val
542 543
544
543 class SpaceSeparatedListOption(Option): 545 class SpaceSeparatedListOption(Option):
544 546
545 """List of space seperated elements. 547 """List of space seperated elements.
546 """ 548 """
547 549
549 551
550 def get(self): 552 def get(self):
551 pathlist = [] 553 pathlist = []
552 _val = Option.get(self) 554 _val = Option.get(self)
553 for elem in _val.split(): 555 for elem in _val.split():
554 pathlist.append(elem) 556 pathlist.append(elem)
555 if pathlist: 557 if pathlist:
556 return pathlist 558 return pathlist
557 else: 559 else:
558 return None 560 return None
559 561
562
560 class OriginHeadersListOption(Option): 563 class OriginHeadersListOption(Option):
561 564
562 """List of space seperated origin header values. 565 """List of space seperated origin header values.
563 """ 566 """
564 567
565 class_description = "A list of space separated case sensitive origin headers 'scheme://host'." 568 class_description = "A list of space separated case sensitive origin headers 'scheme://host'."
566
567 569
568 def set(self, _val): 570 def set(self, _val):
569 pathlist = self._value = [] 571 pathlist = self._value = []
570 for elem in _val.split(): 572 for elem in _val.split():
571 pathlist.append(elem) 573 pathlist.append(elem)
572 if '*' in pathlist and len(pathlist) != 1: 574 if '*' in pathlist and len(pathlist) != 1:
573 raise OptionValueError(self, _val, 575 raise OptionValueError(self, _val,
574 "If using '*' it must be the only element.") 576 "If using '*' it must be the only element.")
575 577
576 def _value2str(self, value): 578 def _value2str(self, value):
577 return ','.join(value) 579 return ','.join(value)
580
578 581
579 class MultiFilePathOption(Option): 582 class MultiFilePathOption(Option):
580 583
581 """List of space seperated File or directory path name 584 """List of space seperated File or directory path name
582 585
682 allow the config file to be stored in version control without 685 allow the config file to be stored in version control without
683 storing the secret there. 686 storing the secret there.
684 687
685 """ 688 """
686 689
687 class_description = \ 690 class_description = (
688 "A string that starts with 'file://' is interpreted as a file path \n" \ 691 "A string that starts with 'file://' is interpreted\n"
689 "relative to the tracker home. Using 'file:///' defines an absolute \n" \ 692 "as a file path relative to the tracker home. Using\n"
690 "path. The first line of the file will be used as the value. Any \n" \ 693 "'file:///' defines an absolute path. The first\n"
691 "string that does not start with 'file://' is used as is. It \n" \ 694 "line of the file will be used as the value. Any\n"
692 "removes any whitespace at the end of the line, so a newline can \n" \ 695 "string that does not start with 'file://' is used\n"
693 "be put in the file.\n" 696 "as is. It removes any whitespace at the end of the\n"
697 "line, so a newline can be put in the file.\n")
694 698
695 def get(self): 699 def get(self):
696 _val = Option.get(self) 700 _val = Option.get(self)
697 if isinstance(_val, str) and _val.startswith('file://'): 701 if isinstance(_val, str) and _val.startswith('file://'):
698 filepath = _val[7:] 702 filepath = _val[7:]
705 # compatible version 709 # compatible version
706 except EnvironmentError as e: 710 except EnvironmentError as e:
707 if e.errno != errno.ENOENT: 711 if e.errno != errno.ENOENT:
708 raise 712 raise
709 else: 713 else:
710 raise OptionValueError(self, _val, 714 raise OptionValueError(
715 self, _val,
711 "Unable to read value for %s. Error opening " 716 "Unable to read value for %s. Error opening "
712 "%s: %s." % (self.name, e.filename, e.args[1])) 717 "%s: %s." % (self.name, e.filename, e.args[1]))
713 return self.str2value(_val) 718 return self.str2value(_val)
714 719
715 def validate(self, options): 720 def validate(self, options):
720 # is set. 725 # is set.
721 try: 726 try:
722 self.get() 727 self.get()
723 except OptionUnsetError: 728 except OptionUnsetError:
724 # provide error message with link to MAIL_USERNAME 729 # provide error message with link to MAIL_USERNAME
725 raise OptionValueError(options["MAIL_PASSWORD"], 730 raise OptionValueError(
726 "not defined", 731 options["MAIL_PASSWORD"],
727 "Mail username is set, so this must be defined.") 732 "not defined",
733 "Mail username is set, so this must be defined.")
728 else: 734 else:
729 self.get() 735 self.get()
730 736
731 737
732 class WebUrlOption(Option): 738 class WebUrlOption(Option):
800 class SecretNullableOption(NullableOption, SecretOption): 806 class SecretNullableOption(NullableOption, SecretOption):
801 # use get from SecretOption and rest from NullableOption 807 # use get from SecretOption and rest from NullableOption
802 get = SecretOption.get 808 get = SecretOption.get
803 class_description = SecretOption.class_description 809 class_description = SecretOption.class_description
804 810
811
805 class RedisUrlOption(SecretNullableOption): 812 class RedisUrlOption(SecretNullableOption):
806 """Do required check to make sure known bad parameters are not 813 """Do required check to make sure known bad parameters are not
807 put in the url. 814 put in the url.
808 815
809 Should I do more URL validation? Validate schema: 816 Should I do more URL validation? Validate schema:
810 redis, rediss, unix? How many cycles to invest 817 redis, rediss, unix? How many cycles to invest
811 to keep users from their own mistakes? 818 to keep users from their own mistakes?
812 """ 819 """
813 820
814 class_description = SecretNullableOption.class_description 821 class_description = SecretNullableOption.class_description
815 822
816 def str2value(self, value): 823 def str2value(self, value):
817 if value and value.find("decode_responses") != -1: 824 if value and value.find("decode_responses") != -1:
818 raise OptionValueError(self, value, "URL must not include " 825 raise OptionValueError(self, value, "URL must not include "
819 "decode_responses. Please remove " 826 "decode_responses. Please remove "
820 "the option.") 827 "the option.")
821 return value 828 return value
829
822 830
823 class SessiondbBackendOption(Option): 831 class SessiondbBackendOption(Option):
824 """Make sure that sessiondb is compatile with the primary db. 832 """Make sure that sessiondb is compatile with the primary db.
825 Fail with error and suggestions if they are incompatible. 833 Fail with error and suggestions if they are incompatible.
826 """ 834 """
850 # unset will choose default 858 # unset will choose default
851 return 859 return
852 860
853 redis_available = False 861 redis_available = False
854 try: 862 try:
855 import redis 863 import redis # noqa: F401
856 redis_available = True 864 redis_available = True
857 except ImportError: 865 except ImportError:
858 if sessiondb_backend == 'redis': 866 if sessiondb_backend == 'redis':
859 valid_session_backends = ', '.join(sorted(list( 867 valid_session_backends = ', '.join(sorted(list(
860 [ x[1] for x in self.compatibility_matrix 868 [x[1] for x in self.compatibility_matrix
861 if x[0] == rdbms_backend and x[1] != 'redis']) 869 if x[0] == rdbms_backend and x[1] != 'redis'])
862 )) 870 ))
863 raise OptionValueError(self, sessiondb_backend, 871 raise OptionValueError(
864 "Unable to load redis module. Please install " 872 self, sessiondb_backend,
865 "a redis library or choose\n an alternate " 873 "Unable to load redis module. Please install "
866 "session db: %(valid_session_backends)s"%locals()) 874 "a redis library or choose\n an alternate "
867 875 "session db: %(valid_session_backends)s" % locals())
868 if ( (rdbms_backend, sessiondb_backend) not in 876
869 self.compatibility_matrix ): 877 if ((rdbms_backend, sessiondb_backend) not in
878 self.compatibility_matrix):
870 879
871 valid_session_backends = ', '.join(sorted(list( 880 valid_session_backends = ', '.join(sorted(list(
872 set([ x[1] for x in self.compatibility_matrix 881 set([x[1] for x in self.compatibility_matrix
873 if x[0] == rdbms_backend and 882 if x[0] == rdbms_backend and
874 ( redis_available or x[1] != 'redis')]) 883 (redis_available or x[1] != 'redis')])
875 ))) 884 )))
876 885
877 raise OptionValueError(self, sessiondb_backend, 886 raise OptionValueError(
887 self, sessiondb_backend,
878 "You can not use session db type: %(sessiondb_backend)s " 888 "You can not use session db type: %(sessiondb_backend)s "
879 "with %(rdbms_backend)s.\n Valid session db types: " 889 "with %(rdbms_backend)s.\n Valid session db types: "
880 "%(valid_session_backends)s."%locals()) 890 "%(valid_session_backends)s." % locals())
881 891
882 892
883 class TimezoneOption(Option): 893 class TimezoneOption(Option):
884 894
885 class_description = \ 895 class_description = \
898 908
899 def str2value(self, value): 909 def str2value(self, value):
900 try: 910 try:
901 roundup.date.get_timezone(value) 911 roundup.date.get_timezone(value)
902 except KeyError: 912 except KeyError:
903 raise OptionValueError(self, value, 913 raise OptionValueError(
904 "Timezone name or numeric hour offset required") 914 self, value,
915 "Timezone name or numeric hour offset required")
905 return value 916 return value
906 917
907 918
908 class HttpVersionOption(Option): 919 class HttpVersionOption(Option):
909 """Used by roundup-server to verify http version is set to valid 920 """Used by roundup-server to verify http version is set to valid
910 string.""" 921 string."""
911 922
912 def str2value(self, value): 923 def str2value(self, value):
913 if value not in ["HTTP/1.0", "HTTP/1.1"]: 924 if value not in ["HTTP/1.0", "HTTP/1.1"]:
914 raise OptionValueError(self, value, 925 raise OptionValueError(
926 self, value,
915 "Valid vaues for -V or --http_version are: HTTP/1.0, HTTP/1.1") 927 "Valid vaues for -V or --http_version are: HTTP/1.0, HTTP/1.1")
916 return value 928 return value
917 929
918 930
919 class RegExpOption(Option): 931 class RegExpOption(Option):
945 value.decode("ascii") 957 value.decode("ascii")
946 except UnicodeError: 958 except UnicodeError:
947 value = value.decode("utf-8") 959 value = value.decode("utf-8")
948 return re.compile(value, self.flags) 960 return re.compile(value, self.flags)
949 961
962
950 try: 963 try:
951 import jinja2 964 import jinja2 # noqa: F401
952 jinja2_avail = "Available found" 965 jinja2_avail = "Available found"
953 except ImportError: 966 except ImportError:
954 jinja2_avail = "Unavailable needs" 967 jinja2_avail = "Unavailable needs"
955 968
956 ### Main configuration layout. 969 # Main configuration layout.
957 # Config is described as a sequence of sections, 970 # Config is described as a sequence of sections,
958 # where each section name is followed by a sequence 971 # where each section name is followed by a sequence
959 # of Option definitions. Each Option definition 972 # of Option definitions. Each Option definition
960 # is a sequence containing class name and constructor 973 # is a sequence containing class name and constructor
961 # parameters, starting from the setting name: 974 # parameters, starting from the setting name:
971 "Templating engine to use.\n" 984 "Templating engine to use.\n"
972 "Possible values are:\n" 985 "Possible values are:\n"
973 " 'zopetal' for the old TAL engine ported from Zope,\n" 986 " 'zopetal' for the old TAL engine ported from Zope,\n"
974 " 'chameleon' for Chameleon,\n" 987 " 'chameleon' for Chameleon,\n"
975 " 'jinja2' for jinja2 templating.\n" 988 " 'jinja2' for jinja2 templating.\n"
976 " %s jinja2 module."%jinja2_avail), 989 " %s jinja2 module." % jinja2_avail),
977 (FilePathOption, "templates", "html", 990 (FilePathOption, "templates", "html",
978 "Path to the HTML templates directory."), 991 "Path to the HTML templates directory."),
979 (MultiFilePathOption, "static_files", "", 992 (MultiFilePathOption, "static_files", "",
980 "A list of space separated directory paths (or a single\n" 993 "A list of space separated directory paths (or a single\n"
981 "directory). These directories hold additional static\n" 994 "directory). These directories hold additional static\n"
1703 "text with <br>. Set true if you want GitHub Flavored Markdown\n" 1716 "text with <br>. Set true if you want GitHub Flavored Markdown\n"
1704 "(GFM) handling of embedded newlines."), 1717 "(GFM) handling of embedded newlines."),
1705 ), "Markdown rendering options."), 1718 ), "Markdown rendering options."),
1706 ) 1719 )
1707 1720
1708 ### Configuration classes 1721 # Configuration classes
1709 1722
1710 1723
1711 class Config: 1724 class Config:
1712 1725
1713 """Base class for configuration objects. 1726 """Base class for configuration objects.
2198 self.init_logging() 2211 self.init_logging()
2199 2212
2200 def init_logging(self): 2213 def init_logging(self):
2201 _file = self["LOGGING_CONFIG"] 2214 _file = self["LOGGING_CONFIG"]
2202 if _file and os.path.isfile(_file): 2215 if _file and os.path.isfile(_file):
2203 logging.config.fileConfig(_file, 2216 logging.config.fileConfig(
2204 disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"]) 2217 _file,
2218 disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"])
2205 return 2219 return
2206 2220
2207 _file = self["LOGGING_FILENAME"] 2221 _file = self["LOGGING_FILENAME"]
2208 # set file & level on the roundup logger 2222 # set file & level on the roundup logger
2209 logger = logging.getLogger('roundup') 2223 logger = logging.getLogger('roundup')

Roundup Issue Tracker: http://roundup-tracker.org/