a11y-moda 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. a11y_moda-0.1.0/LICENSE +21 -0
  2. a11y_moda-0.1.0/PKG-INFO +177 -0
  3. a11y_moda-0.1.0/README.md +139 -0
  4. a11y_moda-0.1.0/pyproject.toml +54 -0
  5. a11y_moda-0.1.0/setup.cfg +4 -0
  6. a11y_moda-0.1.0/src/a11y_moda/__init__.py +3 -0
  7. a11y_moda-0.1.0/src/a11y_moda/_security.py +92 -0
  8. a11y_moda-0.1.0/src/a11y_moda/cli.py +311 -0
  9. a11y_moda-0.1.0/src/a11y_moda/crawler.py +246 -0
  10. a11y_moda-0.1.0/src/a11y_moda/css_utils.py +108 -0
  11. a11y_moda-0.1.0/src/a11y_moda/fetcher.py +103 -0
  12. a11y_moda-0.1.0/src/a11y_moda/llm.py +312 -0
  13. a11y_moda-0.1.0/src/a11y_moda/models.py +72 -0
  14. a11y_moda-0.1.0/src/a11y_moda/report/__init__.py +10 -0
  15. a11y_moda-0.1.0/src/a11y_moda/report/_aggregate.py +73 -0
  16. a11y_moda-0.1.0/src/a11y_moda/report/_common.py +36 -0
  17. a11y_moda-0.1.0/src/a11y_moda/report/_html.py +202 -0
  18. a11y_moda-0.1.0/src/a11y_moda/report/_markdown.py +154 -0
  19. a11y_moda-0.1.0/src/a11y_moda/rules/__init__.py +20 -0
  20. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/__init__.py +1 -0
  21. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/aaa.py +8 -0
  22. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/contrast_focus.py +28 -0
  23. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/css.py +31 -0
  24. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/extension_aria_status.py +9 -0
  25. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/extension_keyboard.py +17 -0
  26. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/extension_media.py +8 -0
  27. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/extension_misc.py +11 -0
  28. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/extension_navigation.py +8 -0
  29. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/extension_presentation.py +9 -0
  30. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/extension_responsive.py +9 -0
  31. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/forms.py +71 -0
  32. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/img_alt.py +19 -0
  33. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/lang.py +8 -0
  34. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/llm_common.py +73 -0
  35. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/llm_consistency.py +9 -0
  36. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/llm_forms.py +10 -0
  37. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/llm_headings.py +9 -0
  38. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/llm_images.py +11 -0
  39. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/llm_links.py +9 -0
  40. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/llm_structure.py +9 -0
  41. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/nav_links.py +22 -0
  42. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/origin.py +80 -0
  43. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/page_meta.py +8 -0
  44. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/structure.py +23 -0
  45. a11y_moda-0.1.0/src/a11y_moda/rules/_lib/vision_rules.py +34 -0
  46. a11y_moda-0.1.0/src/a11y_moda/rules/base.py +114 -0
  47. a11y_moda-0.1.0/src/a11y_moda/rules/codes/__init__.py +1 -0
  48. a11y_moda-0.1.0/src/a11y_moda/rules/codes/aria/AR2410300E.py +38 -0
  49. a11y_moda-0.1.0/src/a11y_moda/rules/codes/aria/AR2410301E.py +43 -0
  50. a11y_moda-0.1.0/src/a11y_moda/rules/codes/aria/AR2410302E.py +43 -0
  51. a11y_moda-0.1.0/src/a11y_moda/rules/codes/aria/AR3130600E.py +38 -0
  52. a11y_moda-0.1.0/src/a11y_moda/rules/codes/aria/FA2410303E.py +40 -0
  53. a11y_moda-0.1.0/src/a11y_moda/rules/codes/aria/__init__.py +1 -0
  54. a11y_moda-0.1.0/src/a11y_moda/rules/codes/consistency/GN1320200E.py +42 -0
  55. a11y_moda-0.1.0/src/a11y_moda/rules/codes/consistency/GN2320300E.py +33 -0
  56. a11y_moda-0.1.0/src/a11y_moda/rules/codes/consistency/GN2320400E.py +33 -0
  57. a11y_moda-0.1.0/src/a11y_moda/rules/codes/consistency/__init__.py +1 -0
  58. a11y_moda-0.1.0/src/a11y_moda/rules/codes/contrast/GN2140300E.py +38 -0
  59. a11y_moda-0.1.0/src/a11y_moda/rules/codes/contrast/GN3140600E.py +43 -0
  60. a11y_moda-0.1.0/src/a11y_moda/rules/codes/contrast/__init__.py +1 -0
  61. a11y_moda-0.1.0/src/a11y_moda/rules/codes/css/CS2140401C.py +53 -0
  62. a11y_moda-0.1.0/src/a11y_moda/rules/codes/css/CS3140801C.py +48 -0
  63. a11y_moda-0.1.0/src/a11y_moda/rules/codes/css/CS3140802C.py +26 -0
  64. a11y_moda-0.1.0/src/a11y_moda/rules/codes/css/__init__.py +1 -0
  65. a11y_moda-0.1.0/src/a11y_moda/rules/codes/focus/CS1140101E.py +53 -0
  66. a11y_moda-0.1.0/src/a11y_moda/rules/codes/focus/CS2240700E.py +30 -0
  67. a11y_moda-0.1.0/src/a11y_moda/rules/codes/focus/FA2141104E.py +31 -0
  68. a11y_moda-0.1.0/src/a11y_moda/rules/codes/focus/GN1240301E.py +33 -0
  69. a11y_moda-0.1.0/src/a11y_moda/rules/codes/focus/__init__.py +1 -0
  70. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1110111E.py +79 -0
  71. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330100E.py +53 -0
  72. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330101E.py +74 -0
  73. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330102E.py +51 -0
  74. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330200E.py +45 -0
  75. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330201E.py +34 -0
  76. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330202E.py +58 -0
  77. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330203E.py +41 -0
  78. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330204E.py +28 -0
  79. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN1330205E.py +38 -0
  80. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN2240601E.py +61 -0
  81. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN2330300E.py +31 -0
  82. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/GN3330602E.py +80 -0
  83. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/HM1130103C.py +43 -0
  84. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/HM1130103C_1.py +48 -0
  85. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/HM1130104C.py +53 -0
  86. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/HM2130500E.py +52 -0
  87. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/HM3330500C.py +25 -0
  88. a11y_moda-0.1.0/src/a11y_moda/rules/codes/forms/__init__.py +1 -0
  89. a11y_moda-0.1.0/src/a11y_moda/rules/codes/headings/GN2240600E.py +58 -0
  90. a11y_moda-0.1.0/src/a11y_moda/rules/codes/headings/HM1130100C.py +78 -0
  91. a11y_moda-0.1.0/src/a11y_moda/rules/codes/headings/HM1130104E.py +48 -0
  92. a11y_moda-0.1.0/src/a11y_moda/rules/codes/headings/HM3241000C.py +27 -0
  93. a11y_moda-0.1.0/src/a11y_moda/rules/codes/headings/__init__.py +1 -0
  94. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110100C.py +45 -0
  95. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110100E.py +63 -0
  96. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110101C.py +41 -0
  97. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110101E.py +54 -0
  98. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110102E.py +45 -0
  99. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110103E.py +51 -0
  100. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110104C.py +39 -0
  101. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110104E.py +44 -0
  102. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110105C.py +49 -0
  103. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110105E.py +46 -0
  104. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110106C.py +33 -0
  105. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110106E.py +45 -0
  106. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110108E.py +45 -0
  107. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/HM1110112E.py +51 -0
  108. a11y_moda-0.1.0/src/a11y_moda/rules/codes/images/__init__.py +1 -0
  109. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/FA1250202E.py +29 -0
  110. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/GN1210100E.py +37 -0
  111. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/GN1210101E.py +71 -0
  112. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/GN1210200E.py +30 -0
  113. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/GN1220200E.py +37 -0
  114. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/GN1250101E.py +32 -0
  115. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/GN1250201E.py +32 -0
  116. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/GN1320100E.py +34 -0
  117. a11y_moda-0.1.0/src/a11y_moda/rules/codes/keyboard/__init__.py +1 -0
  118. a11y_moda-0.1.0/src/a11y_moda/rules/codes/lang/HM1130200C.py +37 -0
  119. a11y_moda-0.1.0/src/a11y_moda/rules/codes/lang/HM2310200C.py +45 -0
  120. a11y_moda-0.1.0/src/a11y_moda/rules/codes/lang/__init__.py +1 -0
  121. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/GN1240401E.py +64 -0
  122. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/HM1240400C.py +38 -0
  123. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/HM1240400E.py +47 -0
  124. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/HM1240401C.py +63 -0
  125. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/HM1240402E.py +41 -0
  126. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/HM1240403E.py +59 -0
  127. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/HM1240404E.py +44 -0
  128. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/HM3240900C.py +50 -0
  129. a11y_moda-0.1.0/src/a11y_moda/rules/codes/links/__init__.py +1 -0
  130. a11y_moda-0.1.0/src/a11y_moda/rules/codes/media/GN1140200E.py +33 -0
  131. a11y_moda-0.1.0/src/a11y_moda/rules/codes/media/GN3120600.py +42 -0
  132. a11y_moda-0.1.0/src/a11y_moda/rules/codes/media/__init__.py +1 -0
  133. a11y_moda-0.1.0/src/a11y_moda/rules/codes/meta/HM1240200C.py +37 -0
  134. a11y_moda-0.1.0/src/a11y_moda/rules/codes/meta/HM1240200E.py +49 -0
  135. a11y_moda-0.1.0/src/a11y_moda/rules/codes/meta/HM1310100C.py +29 -0
  136. a11y_moda-0.1.0/src/a11y_moda/rules/codes/meta/__init__.py +1 -0
  137. a11y_moda-0.1.0/src/a11y_moda/rules/codes/misc/GN1320202E.py +40 -0
  138. a11y_moda-0.1.0/src/a11y_moda/rules/codes/misc/__init__.py +1 -0
  139. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/GN1240100E.py +34 -0
  140. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/GN1240101E.py +32 -0
  141. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/GN1240102E.py +29 -0
  142. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/GN1240103E.py +30 -0
  143. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/GN1240104E.py +32 -0
  144. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/HM1240102C.py +29 -0
  145. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/HM3240800E.py +65 -0
  146. a11y_moda-0.1.0/src/a11y_moda/rules/codes/navigation/__init__.py +1 -0
  147. a11y_moda-0.1.0/src/a11y_moda/rules/codes/presentation/CS1110113E.py +30 -0
  148. a11y_moda-0.1.0/src/a11y_moda/rules/codes/presentation/CS1110114E.py +36 -0
  149. a11y_moda-0.1.0/src/a11y_moda/rules/codes/presentation/CS1130103E.py +33 -0
  150. a11y_moda-0.1.0/src/a11y_moda/rules/codes/presentation/CS1130202E.py +29 -0
  151. a11y_moda-0.1.0/src/a11y_moda/rules/codes/presentation/GN1130201E.py +28 -0
  152. a11y_moda-0.1.0/src/a11y_moda/rules/codes/presentation/__init__.py +1 -0
  153. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2140500E.py +29 -0
  154. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141000E.py +38 -0
  155. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141001E.py +27 -0
  156. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141002E.py +29 -0
  157. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141003E.py +30 -0
  158. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141005E.py +35 -0
  159. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141006E.py +30 -0
  160. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141007E.py +35 -0
  161. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141200E.py +29 -0
  162. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141201E.py +26 -0
  163. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141202E.py +26 -0
  164. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141203E.py +26 -0
  165. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/CS2141204E.py +34 -0
  166. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/FA2130401E.py +34 -0
  167. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/FA2130402E.py +26 -0
  168. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/GN2140400E.py +30 -0
  169. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/GN2140401E.py +27 -0
  170. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/GN2141100E.py +38 -0
  171. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/GN2141101E.py +53 -0
  172. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/GN2141102E.py +34 -0
  173. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/GN2141103E.py +26 -0
  174. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/SC2141004E.py +34 -0
  175. a11y_moda-0.1.0/src/a11y_moda/rules/codes/responsive/__init__.py +1 -0
  176. a11y_moda-0.1.0/src/a11y_moda/rules/codes/semantic/HM1410200C.py +81 -0
  177. a11y_moda-0.1.0/src/a11y_moda/rules/codes/semantic/HM1410201C.py +36 -0
  178. a11y_moda-0.1.0/src/a11y_moda/rules/codes/semantic/__init__.py +1 -0
  179. a11y_moda-0.1.0/src/a11y_moda/rules/codes/structure/GN1130200E.py +43 -0
  180. a11y_moda-0.1.0/src/a11y_moda/rules/codes/structure/HM1130105E.py +23 -0
  181. a11y_moda-0.1.0/src/a11y_moda/rules/codes/structure/__init__.py +1 -0
  182. a11y_moda-0.1.0/src/a11y_moda/rules/codes/tables/HM1130101C.py +36 -0
  183. a11y_moda-0.1.0/src/a11y_moda/rules/codes/tables/HM1130102C.py +65 -0
  184. a11y_moda-0.1.0/src/a11y_moda/rules/codes/tables/HM1130107E.py +31 -0
  185. a11y_moda-0.1.0/src/a11y_moda/rules/codes/tables/HM1130108E.py +51 -0
  186. a11y_moda-0.1.0/src/a11y_moda/rules/codes/tables/HM1130109E.py +30 -0
  187. a11y_moda-0.1.0/src/a11y_moda/rules/codes/tables/HM1130110E.py +39 -0
  188. a11y_moda-0.1.0/src/a11y_moda/rules/codes/tables/__init__.py +1 -0
  189. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/CS1110113E_V.py +47 -0
  190. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/CS1130203E.py +48 -0
  191. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/GN1130102E.py +45 -0
  192. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/GN1130300E.py +49 -0
  193. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/GN1140100E.py +58 -0
  194. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/GN1140102E.py +44 -0
  195. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/GN1240500E.py +44 -0
  196. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/GN2140304E.py +50 -0
  197. a11y_moda-0.1.0/src/a11y_moda/rules/codes/vision/__init__.py +1 -0
  198. a11y_moda-0.1.0/src/a11y_moda/rules/helpers.py +40 -0
  199. a11y_moda-0.1.0/src/a11y_moda/scanner.py +203 -0
  200. a11y_moda-0.1.0/src/a11y_moda/tools/__init__.py +0 -0
  201. a11y_moda-0.1.0/src/a11y_moda/tools/_session.py +71 -0
  202. a11y_moda-0.1.0/src/a11y_moda/tools/contrast.py +226 -0
  203. a11y_moda-0.1.0/src/a11y_moda/tools/form_probe.py +302 -0
  204. a11y_moda-0.1.0/src/a11y_moda/tools/tab_walk.py +77 -0
  205. a11y_moda-0.1.0/src/a11y_moda.egg-info/PKG-INFO +177 -0
  206. a11y_moda-0.1.0/src/a11y_moda.egg-info/SOURCES.txt +208 -0
  207. a11y_moda-0.1.0/src/a11y_moda.egg-info/dependency_links.txt +1 -0
  208. a11y_moda-0.1.0/src/a11y_moda.egg-info/entry_points.txt +2 -0
  209. a11y_moda-0.1.0/src/a11y_moda.egg-info/requires.txt +9 -0
  210. a11y_moda-0.1.0/src/a11y_moda.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 睞特股份有限公司
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: a11y-moda
3
+ Version: 0.1.0
4
+ Summary: Taiwan MODA accessibility CLI · WCAG A/AA/AAA · zh-TW · Freego complement
5
+ Author-email: 睞特股份有限公司 <maple@light-design.com.tw>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/light-design-tw/a11y-moda
8
+ Project-URL: Repository, https://github.com/light-design-tw/a11y-moda
9
+ Project-URL: Issues, https://github.com/light-design-tw/a11y-moda/issues
10
+ Project-URL: Documentation, https://github.com/light-design-tw/a11y-moda#readme
11
+ Project-URL: Changelog, https://github.com/light-design-tw/a11y-moda/blob/main/CHANGELOG.md
12
+ Keywords: accessibility,wcag,a11y,moda,taiwan,freego,audit,scanner
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Natural Language :: Chinese (Traditional)
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
23
+ Classifier: Topic :: Software Development :: Quality Assurance
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: httpx<1.0,>=0.27
29
+ Requires-Dist: beautifulsoup4<5.0,>=4.12
30
+ Requires-Dist: lxml<7.0,>=6.1
31
+ Requires-Dist: click<9.0,>=8.1
32
+ Requires-Dist: playwright<2.0,>=1.59
33
+ Requires-Dist: wcag-contrast-ratio<1.0,>=0.9
34
+ Requires-Dist: tinycss2<2.0,>=1.3
35
+ Requires-Dist: defusedxml<1.0,>=0.7
36
+ Requires-Dist: python-dotenv<2.0,>=1.2.2
37
+ Dynamic: license-file
38
+
39
+ # a11y-moda
40
+
41
+ > 台灣 MODA 無障礙標章自評用 Python CLI · WCAG A / AA / AAA · zh-TW 報告
42
+
43
+ **繁體中文** · [English](https://github.com/light-design-tw/a11y-moda/blob/main/README.en.md)
44
+
45
+ > ⚠️ **非官方社群工具。** 與數位發展部 (MODA / 數位發展部) 無從屬關係,不替代官方 [Freego](https://accessibility.moda.gov.tw/) 與正式審查流程。本工具僅為開發者便利之用。
46
+
47
+ ## 為什麼做這個
48
+
49
+ MODA 官方工具 [Freego](https://accessibility.moda.gov.tw/) 是 Java GUI,沒有 CLI、Docker、API 介面。`a11y-moda` 補這個缺口,給 **CI/CD pipeline** 跟 **AI 協作開發** 用。實作 MODA 公布的規則編碼 (HM / GN / CS / AR / FA / SC),每筆 issue 標注對應 MODA rule_id 跟 WCAG 2.1 success criterion。
50
+
51
+ 人工判斷類規則 (E codes) 接 OpenAI 相容 API。OpenAI、Anthropic、OpenRouter、Ollama、vLLM、LM Studio、llama.cpp server — 任何吐 `/v1/chat/completions` 的端點都接得起來。
52
+
53
+ ## 功能
54
+
55
+ - 靜態掃描 (httpx + BeautifulSoup) 或渲染掃描 (Playwright / headless Chromium,給 SPA 用)
56
+ - A / AA / AAA 等級過濾
57
+ - 全站爬取:先 sitemap.xml,找不到就 BFS
58
+ - 輸出:JSON / Markdown / HTML (HTML 一律渲染依規則 / 依 WCAG / 依 URL 三種 view)
59
+ - LLM 判斷結果存本地 cache — 重跑安全,只有改動的規則重打模型
60
+ - 視覺模型 (VLM) 從截圖驗證版面 / 圖片類規則
61
+ - `--freego-compat` 對齊官方工具回報格式,便於交叉比對
62
+
63
+ ## MODA AAA 自評涵蓋率 (20/20)
64
+
65
+ 實作 MODA AAA 自評表全部 20 題。官方工具對這些 E (人工判斷) 規則的覆蓋率是 **0** — 送件人要逐題手動勾選。本工具自動化 **18 / 20**,剩下 **2** 以 informative caveat 標記,提供人工複查線索。
66
+
67
+ 機制拆解:
68
+
69
+ | 機制 | 數量 | 規則 |
70
+ |---|---|---|
71
+ | **純 DOM** (無外部依賴,毫秒級) | 9 | Q2 GN1110111E (CAPTCHA alt) · Q3 GN3120600 (影片偵測 + caveat) · Q6 AR3130600E (landmark) · Q7 HM1130110E (複雜表格) · Q8 GN1210101E (鍵盤可達) · Q10 GN1240100E (skip link) · Q13 HM3240800E (麵包屑) · Q14 CS2141204E (em 單位) · Q18 HM2130500E (autocomplete) |
72
+ | **LLM (文字)** — OpenAI 相容 | 5 | Q1 HM1110103E (長文 alt) · Q4 HM1130104E (標題巢狀) · Q5 GN2240600E (描述性標題) · Q9 HM1240402E (圖連結文字) · Q17 GN1330201E (必填欄位標示) |
73
+ | **VLM (視覺)** — 多模態 | 1 | Q11 GN1240500E (從首頁截圖偵測網站地圖) |
74
+ | **瀏覽器 probe** (Playwright,無 LLM) | 5 | Q12 CS2240700E (focus visible) · Q15 GN2140300E (AA 對比 4.5:1) · Q16 GN3140600E (AAA 對比 7:1) · Q19 GN3330602E (modal-aware 表單偵測) · Q20 GN2330300E (空送出 → focus 第一個必填無效欄位) |
75
+
76
+ **20 條中 70% 不需要任何 LLM/VLM 呼叫** — 只有 6/20 需要外部模型。LLM 全關,14 條規則照樣有判斷結果。LLM / VLM 端點可指向本地模型 (Ollama, vLLM, LM Studio, qwen3-vl-8b 等),request 不離開內網。
77
+
78
+ > 套件總註冊規則數:**129** (涵蓋 Freego 的 C 類機器檢查 + 我們補的 E 類擴充規則)。上表只是 AAA 自評子集。
79
+
80
+ ## 安裝
81
+
82
+ ```bash
83
+ pip install a11y-moda # PyPI
84
+ playwright install chromium # --render 跟 Playwright probe 都要這個
85
+ ```
86
+
87
+ Python ≥ 3.10。
88
+
89
+ > ⚠️ `pip install` **不會自動下載 Chromium**。第一次跑 `--render` 前必須執行 `playwright install chromium`,否則會噴 `Executable doesn't exist` 錯誤。
90
+
91
+ 開發安裝 (從 source clone):
92
+
93
+ ```bash
94
+ git clone https://github.com/light-design-tw/a11y-moda
95
+ cd a11y-moda
96
+ pip install -e .
97
+ playwright install chromium
98
+ ```
99
+
100
+ ## 快速開始
101
+
102
+ 掃單一 URL:
103
+
104
+ ```bash
105
+ a11y-moda scan https://example.com --level AA
106
+ ```
107
+
108
+ 爬取整站 + 渲染 JS + 用本地 VLM + 輸出 HTML 報告:
109
+
110
+ ```bash
111
+ a11y-moda site https://example.com \
112
+ --level AAA --max-pages 30 --render \
113
+ --llm-base-url http://localhost:8000/v1 --llm-model qwen3-vl-8b \
114
+ --format html -o report.html
115
+ ```
116
+
117
+ LLM endpoint 用環境變數 (沒給 `--llm-*` flag 時 fallback):
118
+
119
+ ```bash
120
+ export A11Y_LLM_BASE_URL=https://api.openai.com/v1
121
+ export A11Y_LLM_KEY=sk-...
122
+ export A11Y_LLM_MODEL=gpt-4o-mini
123
+ ```
124
+
125
+ ## 指令參考
126
+
127
+ ```
128
+ a11y-moda scan <URL> 掃單頁
129
+ a11y-moda site <URL> 探索並掃整站
130
+ ```
131
+
132
+ 常用選項:
133
+
134
+ | Flag | 預設 | 說明 |
135
+ |---|---|---|
136
+ | `--level A\|AA\|AAA` | `AA` | 掃描等級 |
137
+ | `--render` | off | 用 headless Chromium 渲染 JS 頁面 |
138
+ | `--max-pages N` | 30 | `site` 上限 |
139
+ | `--source sitemap\|crawl\|auto` | `auto` | URL 探索策略 |
140
+ | `--workers N` | 4 | 並行 worker (僅靜態掃描;render 強制序列化) |
141
+ | `--rps N` | 0 | 全域速率上限 (req/s),0 = 不限 |
142
+ | `--ignore RULE_ID` | — | 可重複;跳過指定 rule_id |
143
+ | `--freego-only` | off | 只跑官方工具有的機器檢查規則 |
144
+ | `--freego-compat` | off | 對齊官方工具回報 (CS2140401C / CS3140801C / CS3140802C) |
145
+ | `--format json\|md\|html` | `json` | 輸出格式 (也會從 `-o` 副檔名自動判斷) |
146
+ | `-o FILE` | stdout | 純檔名 → `./reports/FILE` |
147
+
148
+ ## 規則怎麼運作
149
+
150
+ 每個 MODA rule_id 一個 Python 檔,放 `src/a11y_moda/rules/codes/<主題>/<RULE_ID>.py`。套件用 `pkgutil.iter_modules` 自動探索;用 `@register` decorator 自動註冊,不用改清單。
151
+
152
+ 樣板:
153
+
154
+ ```python
155
+ from ....models import Level
156
+ from ...base import Rule, RuleMeta, register
157
+ from ...helpers import should_skip, truncate
158
+
159
+ @register
160
+ class MyRule(Rule):
161
+ meta = RuleMeta(rule_id="XX1234567E", guideline="1.1.1", level=Level.A,
162
+ desc="...", source="extension")
163
+ def _check(self, soup, report, *, html, url, ctx) -> None:
164
+ ...
165
+ report.add(self._issue(message="...", snippet="...", status="fail"))
166
+ ```
167
+
168
+ `source = "freego"` → 對應官方工具有的機器檢查規則
169
+ `source = "extension"` → E (人工判斷) 規則被我們程式化的版本
170
+
171
+ ## 專案狀態
172
+
173
+ Pre-1.0。規則覆蓋率對齊 MODA 公布的規則集;LLM 類規則需外部 LLM 接取。1.0 前輸出 schema 可能會變動。
174
+
175
+ ## License
176
+
177
+ [MIT](https://github.com/light-design-tw/a11y-moda/blob/main/LICENSE)
@@ -0,0 +1,139 @@
1
+ # a11y-moda
2
+
3
+ > 台灣 MODA 無障礙標章自評用 Python CLI · WCAG A / AA / AAA · zh-TW 報告
4
+
5
+ **繁體中文** · [English](https://github.com/light-design-tw/a11y-moda/blob/main/README.en.md)
6
+
7
+ > ⚠️ **非官方社群工具。** 與數位發展部 (MODA / 數位發展部) 無從屬關係,不替代官方 [Freego](https://accessibility.moda.gov.tw/) 與正式審查流程。本工具僅為開發者便利之用。
8
+
9
+ ## 為什麼做這個
10
+
11
+ MODA 官方工具 [Freego](https://accessibility.moda.gov.tw/) 是 Java GUI,沒有 CLI、Docker、API 介面。`a11y-moda` 補這個缺口,給 **CI/CD pipeline** 跟 **AI 協作開發** 用。實作 MODA 公布的規則編碼 (HM / GN / CS / AR / FA / SC),每筆 issue 標注對應 MODA rule_id 跟 WCAG 2.1 success criterion。
12
+
13
+ 人工判斷類規則 (E codes) 接 OpenAI 相容 API。OpenAI、Anthropic、OpenRouter、Ollama、vLLM、LM Studio、llama.cpp server — 任何吐 `/v1/chat/completions` 的端點都接得起來。
14
+
15
+ ## 功能
16
+
17
+ - 靜態掃描 (httpx + BeautifulSoup) 或渲染掃描 (Playwright / headless Chromium,給 SPA 用)
18
+ - A / AA / AAA 等級過濾
19
+ - 全站爬取:先 sitemap.xml,找不到就 BFS
20
+ - 輸出:JSON / Markdown / HTML (HTML 一律渲染依規則 / 依 WCAG / 依 URL 三種 view)
21
+ - LLM 判斷結果存本地 cache — 重跑安全,只有改動的規則重打模型
22
+ - 視覺模型 (VLM) 從截圖驗證版面 / 圖片類規則
23
+ - `--freego-compat` 對齊官方工具回報格式,便於交叉比對
24
+
25
+ ## MODA AAA 自評涵蓋率 (20/20)
26
+
27
+ 實作 MODA AAA 自評表全部 20 題。官方工具對這些 E (人工判斷) 規則的覆蓋率是 **0** — 送件人要逐題手動勾選。本工具自動化 **18 / 20**,剩下 **2** 以 informative caveat 標記,提供人工複查線索。
28
+
29
+ 機制拆解:
30
+
31
+ | 機制 | 數量 | 規則 |
32
+ |---|---|---|
33
+ | **純 DOM** (無外部依賴,毫秒級) | 9 | Q2 GN1110111E (CAPTCHA alt) · Q3 GN3120600 (影片偵測 + caveat) · Q6 AR3130600E (landmark) · Q7 HM1130110E (複雜表格) · Q8 GN1210101E (鍵盤可達) · Q10 GN1240100E (skip link) · Q13 HM3240800E (麵包屑) · Q14 CS2141204E (em 單位) · Q18 HM2130500E (autocomplete) |
34
+ | **LLM (文字)** — OpenAI 相容 | 5 | Q1 HM1110103E (長文 alt) · Q4 HM1130104E (標題巢狀) · Q5 GN2240600E (描述性標題) · Q9 HM1240402E (圖連結文字) · Q17 GN1330201E (必填欄位標示) |
35
+ | **VLM (視覺)** — 多模態 | 1 | Q11 GN1240500E (從首頁截圖偵測網站地圖) |
36
+ | **瀏覽器 probe** (Playwright,無 LLM) | 5 | Q12 CS2240700E (focus visible) · Q15 GN2140300E (AA 對比 4.5:1) · Q16 GN3140600E (AAA 對比 7:1) · Q19 GN3330602E (modal-aware 表單偵測) · Q20 GN2330300E (空送出 → focus 第一個必填無效欄位) |
37
+
38
+ **20 條中 70% 不需要任何 LLM/VLM 呼叫** — 只有 6/20 需要外部模型。LLM 全關,14 條規則照樣有判斷結果。LLM / VLM 端點可指向本地模型 (Ollama, vLLM, LM Studio, qwen3-vl-8b 等),request 不離開內網。
39
+
40
+ > 套件總註冊規則數:**129** (涵蓋 Freego 的 C 類機器檢查 + 我們補的 E 類擴充規則)。上表只是 AAA 自評子集。
41
+
42
+ ## 安裝
43
+
44
+ ```bash
45
+ pip install a11y-moda # PyPI
46
+ playwright install chromium # --render 跟 Playwright probe 都要這個
47
+ ```
48
+
49
+ Python ≥ 3.10。
50
+
51
+ > ⚠️ `pip install` **不會自動下載 Chromium**。第一次跑 `--render` 前必須執行 `playwright install chromium`,否則會噴 `Executable doesn't exist` 錯誤。
52
+
53
+ 開發安裝 (從 source clone):
54
+
55
+ ```bash
56
+ git clone https://github.com/light-design-tw/a11y-moda
57
+ cd a11y-moda
58
+ pip install -e .
59
+ playwright install chromium
60
+ ```
61
+
62
+ ## 快速開始
63
+
64
+ 掃單一 URL:
65
+
66
+ ```bash
67
+ a11y-moda scan https://example.com --level AA
68
+ ```
69
+
70
+ 爬取整站 + 渲染 JS + 用本地 VLM + 輸出 HTML 報告:
71
+
72
+ ```bash
73
+ a11y-moda site https://example.com \
74
+ --level AAA --max-pages 30 --render \
75
+ --llm-base-url http://localhost:8000/v1 --llm-model qwen3-vl-8b \
76
+ --format html -o report.html
77
+ ```
78
+
79
+ LLM endpoint 用環境變數 (沒給 `--llm-*` flag 時 fallback):
80
+
81
+ ```bash
82
+ export A11Y_LLM_BASE_URL=https://api.openai.com/v1
83
+ export A11Y_LLM_KEY=sk-...
84
+ export A11Y_LLM_MODEL=gpt-4o-mini
85
+ ```
86
+
87
+ ## 指令參考
88
+
89
+ ```
90
+ a11y-moda scan <URL> 掃單頁
91
+ a11y-moda site <URL> 探索並掃整站
92
+ ```
93
+
94
+ 常用選項:
95
+
96
+ | Flag | 預設 | 說明 |
97
+ |---|---|---|
98
+ | `--level A\|AA\|AAA` | `AA` | 掃描等級 |
99
+ | `--render` | off | 用 headless Chromium 渲染 JS 頁面 |
100
+ | `--max-pages N` | 30 | `site` 上限 |
101
+ | `--source sitemap\|crawl\|auto` | `auto` | URL 探索策略 |
102
+ | `--workers N` | 4 | 並行 worker (僅靜態掃描;render 強制序列化) |
103
+ | `--rps N` | 0 | 全域速率上限 (req/s),0 = 不限 |
104
+ | `--ignore RULE_ID` | — | 可重複;跳過指定 rule_id |
105
+ | `--freego-only` | off | 只跑官方工具有的機器檢查規則 |
106
+ | `--freego-compat` | off | 對齊官方工具回報 (CS2140401C / CS3140801C / CS3140802C) |
107
+ | `--format json\|md\|html` | `json` | 輸出格式 (也會從 `-o` 副檔名自動判斷) |
108
+ | `-o FILE` | stdout | 純檔名 → `./reports/FILE` |
109
+
110
+ ## 規則怎麼運作
111
+
112
+ 每個 MODA rule_id 一個 Python 檔,放 `src/a11y_moda/rules/codes/<主題>/<RULE_ID>.py`。套件用 `pkgutil.iter_modules` 自動探索;用 `@register` decorator 自動註冊,不用改清單。
113
+
114
+ 樣板:
115
+
116
+ ```python
117
+ from ....models import Level
118
+ from ...base import Rule, RuleMeta, register
119
+ from ...helpers import should_skip, truncate
120
+
121
+ @register
122
+ class MyRule(Rule):
123
+ meta = RuleMeta(rule_id="XX1234567E", guideline="1.1.1", level=Level.A,
124
+ desc="...", source="extension")
125
+ def _check(self, soup, report, *, html, url, ctx) -> None:
126
+ ...
127
+ report.add(self._issue(message="...", snippet="...", status="fail"))
128
+ ```
129
+
130
+ `source = "freego"` → 對應官方工具有的機器檢查規則
131
+ `source = "extension"` → E (人工判斷) 規則被我們程式化的版本
132
+
133
+ ## 專案狀態
134
+
135
+ Pre-1.0。規則覆蓋率對齊 MODA 公布的規則集;LLM 類規則需外部 LLM 接取。1.0 前輸出 schema 可能會變動。
136
+
137
+ ## License
138
+
139
+ [MIT](https://github.com/light-design-tw/a11y-moda/blob/main/LICENSE)
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "a11y-moda"
3
+ version = "0.1.0"
4
+ description = "Taiwan MODA accessibility CLI · WCAG A/AA/AAA · zh-TW · Freego complement"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "睞特股份有限公司", email = "maple@light-design.com.tw" },
10
+ ]
11
+ keywords = ["accessibility", "wcag", "a11y", "moda", "taiwan", "freego", "audit", "scanner"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Natural Language :: Chinese (Traditional)",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Internet :: WWW/HTTP :: Site Management",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ "Topic :: Software Development :: Testing",
25
+ ]
26
+ requires-python = ">=3.10"
27
+ dependencies = [
28
+ "httpx>=0.27,<1.0",
29
+ "beautifulsoup4>=4.12,<5.0",
30
+ "lxml>=6.1,<7.0", # CVE-2026-41066 (XXE) fixed in 6.1
31
+ "click>=8.1,<9.0",
32
+ "playwright>=1.59,<2.0", # CVE-2026-2441 Chromium use-after-free patched in 1.59
33
+ "wcag-contrast-ratio>=0.9,<1.0",
34
+ "tinycss2>=1.3,<2.0",
35
+ "defusedxml>=0.7,<1.0",
36
+ "python-dotenv>=1.2.2,<2.0", # CVE-2026-28684 symlink in set_key/unset_key
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/light-design-tw/a11y-moda"
41
+ Repository = "https://github.com/light-design-tw/a11y-moda"
42
+ Issues = "https://github.com/light-design-tw/a11y-moda/issues"
43
+ Documentation = "https://github.com/light-design-tw/a11y-moda#readme"
44
+ Changelog = "https://github.com/light-design-tw/a11y-moda/blob/main/CHANGELOG.md"
45
+
46
+ [project.scripts]
47
+ a11y-moda = "a11y_moda.cli:main"
48
+
49
+ [build-system]
50
+ requires = ["setuptools>=77"]
51
+ build-backend = "setuptools.build_meta"
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ __version__ = "0.1.0"
2
+
3
+ USER_AGENT = f"Mozilla/5.0 (compatible; a11y-moda/{__version__}; +https://www.light-design.com.tw)"
@@ -0,0 +1,92 @@
1
+ """URL safety checks (SSRF defence)."""
2
+ from __future__ import annotations
3
+ import ipaddress
4
+ import os
5
+ import socket
6
+ from urllib.parse import urlparse
7
+
8
+
9
+ _BLOCKED_HOSTS = {"localhost", "0.0.0.0", "::", "::1", "ip6-localhost", "ip6-loopback"}
10
+
11
+
12
+ class UnsafeURLError(ValueError):
13
+ """URL was rejected because it points at a private / loopback / non-http host."""
14
+
15
+
16
+ def _allow_private_default() -> bool:
17
+ """Operator can opt in via env var when scanning intranets."""
18
+ return os.environ.get("A11Y_ALLOW_PRIVATE_HOSTS", "").strip() == "1"
19
+
20
+
21
+ def _normalise_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address):
22
+ """Unwrap IPv4-mapped IPv6 (`::ffff:127.0.0.1`) so private/loopback checks
23
+ land on the embedded IPv4. ipaddress's is_loopback is False on the wrapper.
24
+ """
25
+ if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped is not None:
26
+ return ip.ipv4_mapped
27
+ return ip
28
+
29
+
30
+ def _ip_is_private(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
31
+ ip = _normalise_ip(ip)
32
+ return (ip.is_private or ip.is_loopback or ip.is_link_local
33
+ or ip.is_reserved or ip.is_multicast or ip.is_unspecified)
34
+
35
+
36
+ def _resolve_all(host: str) -> list[ipaddress.IPv4Address | ipaddress.IPv6Address]:
37
+ """All IPs the resolver returns. Empty list on failure (callers treat as unsafe)."""
38
+ try:
39
+ infos = socket.getaddrinfo(host, None)
40
+ except socket.gaierror:
41
+ return []
42
+ out = []
43
+ for info in infos:
44
+ ip_str = info[4][0]
45
+ try:
46
+ out.append(ipaddress.ip_address(ip_str))
47
+ except ValueError:
48
+ continue
49
+ return out
50
+
51
+
52
+ def is_safe_http_url(url: str, *, allow_private: bool | None = None) -> bool:
53
+ """True when URL is safe to fetch from arbitrary callers.
54
+
55
+ Rejects: non-http(s) schemes, missing host, loopback / private / link-local
56
+ / reserved / multicast IP literals, IPv4-mapped IPv6 wrappers around such
57
+ IPs, and hostnames where ANY resolved IP falls in those ranges (mitigates
58
+ DNS rebinding where the resolver returns both a public and a private IP).
59
+
60
+ Pass allow_private=True (or set A11Y_ALLOW_PRIVATE_HOSTS=1) to permit
61
+ intranet scans.
62
+ """
63
+ if allow_private is None:
64
+ allow_private = _allow_private_default()
65
+ try:
66
+ p = urlparse(url)
67
+ except Exception:
68
+ return False
69
+ if p.scheme not in ("http", "https"):
70
+ return False
71
+ host = (p.hostname or "").lower()
72
+ if not host:
73
+ return False
74
+ if allow_private:
75
+ return True
76
+ if host in _BLOCKED_HOSTS:
77
+ return False
78
+ try:
79
+ ip = ipaddress.ip_address(host)
80
+ return not _ip_is_private(ip)
81
+ except ValueError:
82
+ pass # hostname, resolve below
83
+ ips = _resolve_all(host)
84
+ if not ips:
85
+ return False
86
+ return not any(_ip_is_private(ip) for ip in ips)
87
+
88
+
89
+ def require_safe_http_url(url: str, *, allow_private: bool | None = None) -> None:
90
+ """Raise UnsafeURLError if the URL is not safe."""
91
+ if not is_safe_http_url(url, allow_private=allow_private):
92
+ raise UnsafeURLError(f"refused unsafe URL (private/loopback/non-http): {url!r}")