synapse 2.184.0__py311-none-any.whl → 2.186.0__py311-none-any.whl

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.

Potentially problematic release.


This version of synapse might be problematic. Click here for more details.

Files changed (49) hide show
  1. synapse/cortex.py +4 -3
  2. synapse/datamodel.py +41 -6
  3. synapse/exc.py +2 -0
  4. synapse/lib/ast.py +83 -22
  5. synapse/lib/auth.py +13 -0
  6. synapse/lib/cell.py +78 -2
  7. synapse/lib/drive.py +45 -10
  8. synapse/lib/modules.py +1 -0
  9. synapse/lib/parser.py +1 -0
  10. synapse/lib/snap.py +1 -6
  11. synapse/lib/storm.lark +12 -6
  12. synapse/lib/storm.py +45 -9
  13. synapse/lib/storm_format.py +1 -0
  14. synapse/lib/stormlib/stix.py +14 -5
  15. synapse/lib/stormtypes.py +64 -36
  16. synapse/lib/types.py +6 -0
  17. synapse/lib/version.py +2 -2
  18. synapse/models/doc.py +93 -0
  19. synapse/models/infotech.py +2 -1
  20. synapse/models/media.py +0 -1
  21. synapse/models/orgs.py +26 -3
  22. synapse/models/proj.py +56 -36
  23. synapse/models/risk.py +3 -0
  24. synapse/models/syn.py +64 -6
  25. synapse/tests/test_cortex.py +49 -6
  26. synapse/tests/test_lib_base.py +2 -2
  27. synapse/tests/test_lib_cell.py +59 -5
  28. synapse/tests/test_lib_grammar.py +2 -0
  29. synapse/tests/test_lib_storm.py +54 -1
  30. synapse/tests/test_lib_stormlib_stix.py +3 -2
  31. synapse/tests/test_model_doc.py +51 -0
  32. synapse/tests/test_model_orgs.py +41 -0
  33. synapse/tests/test_model_risk.py +2 -0
  34. synapse/tests/test_model_syn.py +43 -0
  35. synapse/tests/test_tools_promote.py +67 -0
  36. synapse/tests/test_tools_snapshot.py +47 -0
  37. synapse/tools/aha/clone.py +3 -1
  38. synapse/tools/aha/easycert.py +1 -1
  39. synapse/tools/aha/enroll.py +3 -1
  40. synapse/tools/aha/provision/service.py +3 -1
  41. synapse/tools/aha/provision/user.py +3 -1
  42. synapse/tools/livebackup.py +3 -1
  43. synapse/tools/promote.py +23 -4
  44. synapse/tools/snapshot.py +69 -0
  45. {synapse-2.184.0.dist-info → synapse-2.186.0.dist-info}/METADATA +5 -10
  46. {synapse-2.184.0.dist-info → synapse-2.186.0.dist-info}/RECORD +49 -44
  47. {synapse-2.184.0.dist-info → synapse-2.186.0.dist-info}/WHEEL +1 -1
  48. {synapse-2.184.0.dist-info → synapse-2.186.0.dist-info}/LICENSE +0 -0
  49. {synapse-2.184.0.dist-info → synapse-2.186.0.dist-info}/top_level.txt +0 -0
@@ -701,6 +701,47 @@ class OuModelTest(s_t_utils.SynTest):
701
701
  self.len(1, await core.nodes('ou:asset :owner -> ps:contact +:name=foo '))
702
702
  self.len(1, await core.nodes('ou:asset :operator -> ps:contact +:name=bar '))
703
703
 
704
+ visi = await core.auth.addUser('visi')
705
+
706
+ nodes = await core.nodes('''
707
+ [ ou:enacted=*
708
+ :id=V-99
709
+ :project={[ proj:project=* ]}
710
+ :status=10
711
+ :priority=highest
712
+ :created=20241018
713
+ :updated=20241018
714
+ :due=20241018
715
+ :completed=20241018
716
+ :creator=root
717
+ :assignee=visi
718
+ :scope=(ou:team, *)
719
+ :ext:creator={[ ps:contact=* :name=root ]}
720
+ :ext:assignee={[ ps:contact=* :name=visi ]}
721
+ ]
722
+ ''')
723
+ self.len(1, nodes)
724
+ self.eq('V-99', nodes[0].get('id'))
725
+ self.eq(10, nodes[0].get('status'))
726
+ self.eq(50, nodes[0].get('priority'))
727
+
728
+ self.eq(1729209600000, nodes[0].get('due'))
729
+ self.eq(1729209600000, nodes[0].get('created'))
730
+ self.eq(1729209600000, nodes[0].get('updated'))
731
+ self.eq(1729209600000, nodes[0].get('completed'))
732
+
733
+ self.eq(visi.iden, nodes[0].get('assignee'))
734
+ self.eq(core.auth.rootuser.iden, nodes[0].get('creator'))
735
+
736
+ self.nn(nodes[0].get('scope'))
737
+ self.nn(nodes[0].get('ext:creator'))
738
+ self.nn(nodes[0].get('ext:assignee'))
739
+
740
+ self.len(1, await core.nodes('ou:enacted -> proj:project'))
741
+ self.len(1, await core.nodes('ou:enacted :scope -> ou:team'))
742
+ self.len(1, await core.nodes('ou:enacted :ext:creator -> ps:contact +:name=root'))
743
+ self.len(1, await core.nodes('ou:enacted :ext:assignee -> ps:contact +:name=visi'))
744
+
704
745
  async def test_ou_code_prefixes(self):
705
746
  guid0 = s_common.guid()
706
747
  guid1 = s_common.guid()
@@ -577,6 +577,7 @@ class RiskModelTest(s_t_utils.SynTest):
577
577
  :techniques=(*,)
578
578
  :tag=cno.mal.cobaltstrike
579
579
  :mitre:attack:software=S0001
580
+ :id=" AAAbbb123 "
580
581
 
581
582
  :sophistication=high
582
583
  :availability=public
@@ -593,6 +594,7 @@ class RiskModelTest(s_t_utils.SynTest):
593
594
  self.eq(1643673600000, nodes[0].get('reporter:discovered'))
594
595
  self.eq(1675209600000, nodes[0].get('reporter:published'))
595
596
  self.eq('S0001', nodes[0].get('mitre:attack:software'))
597
+ self.eq('AAAbbb123', nodes[0].get('id'))
596
598
 
597
599
  self.eq('cobaltstrike', nodes[0].get('soft:name'))
598
600
  self.eq(('beacon',), nodes[0].get('soft:names'))
@@ -1,5 +1,7 @@
1
1
  import synapse.exc as s_exc
2
+ import synapse.common as s_common
2
3
  import synapse.cortex as s_cortex
4
+ import synapse.datamodel as s_datamodel
3
5
 
4
6
  import synapse.lib.stormsvc as s_stormsvc
5
7
 
@@ -46,6 +48,47 @@ class TestService(s_stormsvc.StormSvc):
46
48
 
47
49
  class SynModelTest(s_t_utils.SynTest):
48
50
 
51
+ async def test_syn_userrole(self):
52
+
53
+ async with self.getTestCore() as core:
54
+
55
+ (ok, iden) = await core.callStorm('return($lib.trycast(syn:user, root))')
56
+ self.true(ok)
57
+ self.eq(iden, core.auth.rootuser.iden)
58
+
59
+ # coverage for iden taking precedence
60
+ (ok, iden) = await core.callStorm(f'return($lib.trycast(syn:user, {iden}))')
61
+ self.true(ok)
62
+ self.eq(iden, core.auth.rootuser.iden)
63
+
64
+ self.eq('root', await core.callStorm(f'return($lib.repr(syn:user, {iden}))'))
65
+
66
+ (ok, iden) = await core.callStorm('return($lib.trycast(syn:role, all))')
67
+ self.true(ok)
68
+ self.eq(iden, core.auth.allrole.iden)
69
+
70
+ # coverage for iden taking precedence
71
+ (ok, iden) = await core.callStorm(f'return($lib.trycast(syn:role, {iden}))')
72
+ self.true(ok)
73
+ self.eq(iden, core.auth.allrole.iden)
74
+
75
+ self.eq('all', await core.callStorm(f'return($lib.repr(syn:role, {iden}))'))
76
+
77
+ # coverage for DataModel without a cortex reference
78
+ iden = s_common.guid()
79
+
80
+ model = core.model
81
+ model.core = None
82
+
83
+ synuser = model.type('syn:user')
84
+ synrole = model.type('syn:user')
85
+
86
+ self.eq(iden, synuser.repr(iden))
87
+ self.eq(iden, synrole.repr(iden))
88
+
89
+ self.eq(iden, synuser.norm(iden)[0])
90
+ self.eq(iden, synrole.norm(iden)[0])
91
+
49
92
  async def test_syn_tag(self):
50
93
 
51
94
  async with self.getTestCore() as core:
@@ -0,0 +1,67 @@
1
+ import synapse.common as s_common
2
+
3
+ import synapse.lib.base as s_base
4
+ import synapse.lib.cell as s_cell
5
+
6
+ import synapse.tools.promote as s_tools_promote
7
+
8
+ import synapse.tests.utils as s_t_utils
9
+
10
+
11
+ class PromoteToolTest(s_t_utils.SynTest):
12
+
13
+ async def test_tool_promote_simple(self):
14
+ async with self.getTestAha() as aha:
15
+ async with await s_base.Base.anit() as base:
16
+ with self.getTestDir() as dirn:
17
+ dirn00 = s_common.genpath(dirn, '00.cell')
18
+ dirn01 = s_common.genpath(dirn, '01.cell')
19
+
20
+ cell00 = await base.enter_context(self.addSvcToAha(aha, '00.cell', s_cell.Cell, dirn=dirn00))
21
+ cell01 = await base.enter_context(self.addSvcToAha(aha, '01.cell', s_cell.Cell, dirn=dirn01,
22
+ provinfo={'mirror': 'cell'}))
23
+ self.true(cell00.isactive)
24
+ self.false(cell01.isactive)
25
+ await cell01.sync()
26
+
27
+ outp = self.getTestOutp()
28
+ argv = ['--svcurl', cell00.getLocalUrl()]
29
+ ret = await s_tools_promote.main(argv, outp=outp)
30
+ self.eq(1, ret)
31
+ outp.expect('Failed to promote service')
32
+ outp.expect('promote() called on non-mirror')
33
+
34
+ outp.clear()
35
+ argv = ['--svcurl', cell01.getLocalUrl()]
36
+ ret = await s_tools_promote.main(argv, outp=outp)
37
+ self.eq(0, ret)
38
+ self.false(cell00.isactive)
39
+ self.true(cell01.isactive)
40
+ await cell00.sync()
41
+
42
+ async def test_tool_promote_schism(self):
43
+ # Create a mirror of mirrors and try promoting the end mirror.
44
+ async with self.getTestAha() as aha:
45
+ async with await s_base.Base.anit() as base:
46
+ with self.getTestDir() as dirn:
47
+ dirn00 = s_common.genpath(dirn, '00.cell')
48
+ dirn01 = s_common.genpath(dirn, '01.cell')
49
+ dirn02 = s_common.genpath(dirn, '02.cell')
50
+
51
+ cell00 = await base.enter_context(self.addSvcToAha(aha, '00.cell', s_cell.Cell, dirn=dirn00))
52
+ cell01 = await base.enter_context(self.addSvcToAha(aha, '01.cell', s_cell.Cell, dirn=dirn01,
53
+ provinfo={'mirror': '00.cell'}))
54
+ cell02 = await base.enter_context(self.addSvcToAha(aha, '02.cell', s_cell.Cell, dirn=dirn02,
55
+ provinfo={'mirror': '01.cell'}))
56
+ self.true(cell00.isactive)
57
+ self.false(cell01.isactive)
58
+ self.false(cell02.isactive)
59
+ await cell02.sync()
60
+
61
+ outp = self.getTestOutp()
62
+ argv = ['--svcurl', cell02.getLocalUrl()]
63
+ ret = await s_tools_promote.main(argv, outp=outp)
64
+ self.eq(1, ret)
65
+ outp.expect('Failed to promote service')
66
+ # Note: The following message may change when SYN-7659 is addressed
67
+ outp.expect('ahaname=01.cell is not the current leader and cannot handoff leadership to aha://02.cell.synapse')
@@ -0,0 +1,47 @@
1
+ from unittest import mock
2
+
3
+ import synapse.lib.output as s_output
4
+ import synapse.tools.snapshot as s_tools_snapshot
5
+
6
+ import synapse.tests.utils as s_t_utils
7
+
8
+ class PromoteToolTest(s_t_utils.SynTest):
9
+
10
+ async def test_tool_snapshot(self):
11
+
12
+ async with self.getTestCore() as core:
13
+
14
+ lurl = core.getLocalUrl()
15
+
16
+ self.eq(0, await s_tools_snapshot.main(('freeze', '--svcurl', lurl)))
17
+ self.true(core.paused)
18
+
19
+ outp = s_output.OutPutStr()
20
+ self.eq(1, await s_tools_snapshot.main(('freeze', '--svcurl', lurl), outp=outp))
21
+ self.isin('ERROR BadState', str(outp))
22
+
23
+ self.eq(0, await s_tools_snapshot.main(('resume', '--svcurl', lurl)))
24
+ self.false(core.paused)
25
+
26
+ outp = s_output.OutPutStr()
27
+ self.eq(1, await s_tools_snapshot.main(('resume', '--svcurl', lurl), outp=outp))
28
+ self.isin('ERROR BadState', str(outp))
29
+
30
+ outp = s_output.OutPutStr()
31
+ async with core.nexslock:
32
+ argv = ('freeze', '--svcurl', lurl, '--timeout', '1')
33
+ self.eq(1, await s_tools_snapshot.main(argv, outp=outp))
34
+ self.isin('ERROR TimeOut', str(outp))
35
+
36
+ def boom():
37
+ raise Exception('boom')
38
+
39
+ outp = s_output.OutPutStr()
40
+ with mock.patch('os.sync', boom):
41
+ self.eq(1, await s_tools_snapshot.main(('freeze', '--svcurl', lurl), outp=outp))
42
+ self.false(core.paused)
43
+ self.isin('ERROR SynErr: boom', str(outp))
44
+
45
+ outp = s_output.OutPutStr()
46
+ self.eq(1, await s_tools_snapshot.main(('freeze', '--svcurl', 'newp://newp'), outp=outp))
47
+ self.isin('ERROR BadUrl', str(outp))
@@ -18,7 +18,9 @@ Examples:
18
18
 
19
19
  async def main(argv, outp=s_output.stdout):
20
20
 
21
- pars = argparse.ArgumentParser(prog='synapse.tools.aha.clone', description=descr)
21
+ pars = argparse.ArgumentParser(prog='synapse.tools.aha.clone', description=descr,
22
+ formatter_class=argparse.RawDescriptionHelpFormatter)
23
+
22
24
  pars.add_argument('dnsname', help='The DNS name of the new AHA server.')
23
25
  pars.add_argument('--port', type=int, default=27492, help='The port that the new AHA server should listen on.')
24
26
  pars.add_argument('--url', default='cell:///vertex/storage', help='The telepath URL to connect to the AHA service.')
@@ -51,7 +51,7 @@ async def _main(argv, outp):
51
51
 
52
52
  def getArgParser():
53
53
  desc = 'CLI tool to generate simple x509 certificates from an Aha server.'
54
- pars = argparse.ArgumentParser(prog='aha.easycert', description=desc)
54
+ pars = argparse.ArgumentParser(prog='synapse.tools.aha.easycert', description=desc)
55
55
 
56
56
  pars.add_argument('-a', '--aha', required=True, # type=str,
57
57
  help='Aha server to connect too.')
@@ -21,7 +21,9 @@ Examples:
21
21
 
22
22
  async def main(argv, outp=s_output.stdout):
23
23
 
24
- pars = argparse.ArgumentParser(prog='provision', description=descr)
24
+ pars = argparse.ArgumentParser(prog='synapse.tools.aha.enroll', description=descr,
25
+ formatter_class=argparse.RawDescriptionHelpFormatter)
26
+
25
27
  pars.add_argument('onceurl', help='The one-time use AHA user enrollment URL.')
26
28
  opts = pars.parse_args(argv)
27
29
 
@@ -23,7 +23,9 @@ Examples:
23
23
 
24
24
  async def main(argv, outp=s_output.stdout):
25
25
 
26
- pars = argparse.ArgumentParser(prog='synapse.tools.aha.provision.service', description=descr)
26
+ pars = argparse.ArgumentParser(prog='synapse.tools.aha.provision.service', description=descr,
27
+ formatter_class=argparse.RawDescriptionHelpFormatter)
28
+
27
29
  pars.add_argument('--url', default='cell:///vertex/storage', help='The telepath URL to connect to the AHA service.')
28
30
  pars.add_argument('--user', help='Provision the new service with the username.')
29
31
  pars.add_argument('--cellyaml', help='Specify the path to a YAML file containing config options for the service.')
@@ -22,7 +22,9 @@ Examples:
22
22
 
23
23
  async def main(argv, outp=s_output.stdout):
24
24
 
25
- pars = argparse.ArgumentParser(prog='synapse.tools.aha.provision.user', description=descr)
25
+ pars = argparse.ArgumentParser(prog='synapse.tools.aha.provision.user', description=descr,
26
+ formatter_class=argparse.RawDescriptionHelpFormatter)
27
+
26
28
  pars.add_argument('--url', default='cell:///vertex/storage', help='The telepath URL to connect to the AHA service.')
27
29
  pars.add_argument('--again', default=False, action='store_true', help='Generate a new enroll URL for an existing user.')
28
30
  pars.add_argument('--only-url', help='Only output the URL upon successful execution',
@@ -20,7 +20,9 @@ Examples:
20
20
 
21
21
  async def main(argv, outp=s_output.stdout):
22
22
 
23
- pars = argparse.ArgumentParser(prog='livebackup', description=descr)
23
+ pars = argparse.ArgumentParser(prog='synapse.tools.livebackup', description=descr,
24
+ formatter_class=argparse.RawDescriptionHelpFormatter)
25
+
24
26
  pars.add_argument('--url', default='cell:///vertex/storage', help='The telepath URL of the Synapse service.')
25
27
  pars.add_argument('--name', default=None, help='Specify a name for the backup. Defaults to an automatically generated timestamp.')
26
28
 
synapse/tools/promote.py CHANGED
@@ -2,9 +2,12 @@ import sys
2
2
  import asyncio
3
3
  import argparse
4
4
 
5
+ import synapse.exc as s_exc
6
+
5
7
  import synapse.telepath as s_telepath
6
8
 
7
9
  import synapse.lib.output as s_output
10
+ import synapse.lib.urlhelp as s_urlhelp
8
11
 
9
12
  descr = '''
10
13
  Promote a mirror to the leader.
@@ -15,9 +18,15 @@ Example (being run from a Cortex mirror docker container):
15
18
 
16
19
  async def main(argv, outp=s_output.stdout):
17
20
 
18
- pars = argparse.ArgumentParser(prog='provision', description=descr)
19
- pars.add_argument('--svcurl', default='cell:///vertex/storage', help='The telepath URL of the Synapse service.')
20
- pars.add_argument('--failure', default=False, action='store_true', help='Promotion is due to leader being offline. Graceful handoff is not possible.')
21
+ pars = argparse.ArgumentParser(prog='synapse.tools.promote', description=descr,
22
+ formatter_class=argparse.RawDescriptionHelpFormatter)
23
+
24
+ pars.add_argument('--svcurl', default='cell:///vertex/storage',
25
+ help='The telepath URL of the Synapse service.')
26
+
27
+ pars.add_argument('--failure', default=False, action='store_true',
28
+ help='Promotion is due to leader being offline. Graceful handoff is not possible.')
29
+
21
30
  # TODO pars.add_argument('--timeout', type=float, default=30.0, help='The maximum timeout to wait for the mirror to catch up.')
22
31
 
23
32
  opts = pars.parse_args(argv)
@@ -29,7 +38,17 @@ async def main(argv, outp=s_output.stdout):
29
38
  graceful = not opts.failure
30
39
 
31
40
  outp.printf(f'Promoting to leader: {opts.svcurl}')
32
- await cell.promote(graceful=graceful)
41
+ try:
42
+ await cell.promote(graceful=graceful)
43
+ except s_exc.BadState as e:
44
+ mesg = f'Failed to promote service to being a leader; {e.get("mesg")}'
45
+ outp.printf(mesg)
46
+ return 1
47
+ except s_exc.SynErr as e:
48
+ outp.printf(f'Failed to promote service {s_urlhelp.sanitizeUrl(opts.svcurl)}: {e}')
49
+ return 1
50
+
51
+ return 0
33
52
 
34
53
  if __name__ == '__main__': # pragma: no cover
35
54
  sys.exit(asyncio.run(main(sys.argv[1:])))
@@ -0,0 +1,69 @@
1
+ import sys
2
+ import asyncio
3
+ import logging
4
+ import argparse
5
+
6
+ import synapse.exc as s_exc
7
+ import synapse.telepath as s_telepath
8
+
9
+ import synapse.lib.output as s_output
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ desc = '''
14
+ Command line tool to freeze/resume service operations to allow
15
+ system admins to generate a transactionally consistent volume
16
+ snapshot using 3rd party tools.
17
+
18
+ The use pattern should be::
19
+
20
+ python -m synapse.tools.snapshot freeze
21
+
22
+ <generate volume snapshot using 3rd party tools>
23
+
24
+ python -m synapse.tools.snapshot resume
25
+
26
+ The tool will set the process exit code to 0 on success.
27
+ '''
28
+
29
+ async def main(argv, outp=s_output.stdout):
30
+
31
+ pars = argparse.ArgumentParser('synapse.tools.snapshot',
32
+ description=desc,
33
+ formatter_class=argparse.RawDescriptionHelpFormatter)
34
+
35
+ subs = pars.add_subparsers(required=True, title='commands', dest='cmd')
36
+
37
+ freeze = subs.add_parser('freeze', help='Suspend edits and sync changes to disk.')
38
+ freeze.add_argument('--timeout', type=int, default=120,
39
+ help='Maximum time to wait for the nexus lock.')
40
+
41
+ freeze.add_argument('--svcurl', default='cell:///vertex/storage',
42
+ help='The telepath URL of the Synapse service.')
43
+
44
+ resume = subs.add_parser('resume', help='Resume edits and continue normal operation.')
45
+ resume.add_argument('--svcurl', default='cell:///vertex/storage',
46
+ help='The telepath URL of the Synapse service.')
47
+
48
+ opts = pars.parse_args(argv)
49
+
50
+ try:
51
+ async with s_telepath.withTeleEnv():
52
+
53
+ async with await s_telepath.openurl(opts.svcurl) as proxy:
54
+
55
+ if opts.cmd == 'freeze':
56
+ await proxy.freeze(timeout=opts.timeout)
57
+ return 0
58
+
59
+ if opts.cmd == 'resume':
60
+ await proxy.resume()
61
+ return 0
62
+
63
+ except s_exc.SynErr as e:
64
+ mesg = e.errinfo.get('mesg')
65
+ outp.printf(f'ERROR {e.__class__.__name__}: {mesg}')
66
+ return 1
67
+
68
+ if __name__ == '__main__': # pragma: no cover
69
+ sys.exit(asyncio.run(main(sys.argv[1:])))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synapse
3
- Version: 2.184.0
3
+ Version: 2.186.0
4
4
  Summary: Synapse Intelligence Analysis Framework
5
5
  Author-email: The Vertex Project LLC <root@vertex.link>
6
6
  License: Apache License 2.0
@@ -59,15 +59,10 @@ Requires-Dist: bump2version<1.1.0,>=1.0.1; extra == "dev"
59
59
  Requires-Dist: pytest-xdist<4.0.0,>=3.0.2; extra == "dev"
60
60
  Requires-Dist: coverage<8.0.0,>=7.0.0; extra == "dev"
61
61
  Provides-Extra: docs
62
- Requires-Dist: nbconvert<8.0.0,>=7.3.1; extra == "docs"
63
- Requires-Dist: jupyter-client<=8.2.0; extra == "docs"
64
- Requires-Dist: jupyter<2.0.0,>=1.0.0; extra == "docs"
65
- Requires-Dist: hide-code<0.8.0,>=0.7.0; extra == "docs"
66
- Requires-Dist: nbstripout<1.0.0,>=0.3.3; extra == "docs"
67
- Requires-Dist: sphinx<7.0.0,>=6.2.0; extra == "docs"
68
- Requires-Dist: sphinx-rtd-theme<2.0.0,>=1.0.0; extra == "docs"
69
- Requires-Dist: sphinx-notfound-page==0.8.3; extra == "docs"
70
- Requires-Dist: jinja2<3.1.0; extra == "docs"
62
+ Requires-Dist: sphinx<9.0.0,>=8.0.0; extra == "docs"
63
+ Requires-Dist: sphinx-rtd-theme<4.0.0,>=3.0.0; extra == "docs"
64
+ Requires-Dist: sphinx-notfound-page<2.0.0,>=1.0.4; extra == "docs"
65
+ Requires-Dist: jinja2<4.0.0,>=3.1.4; extra == "docs"
71
66
 
72
67
  Synapse
73
68
  =======