intermediate Medium/Resource refactoring
This commit is contained in:
		
							parent
							
								
									b726f495b6
								
							
						
					
					
						commit
						090c122c60
					
				
							
								
								
									
										179
									
								
								co3/co3.py
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								co3/co3.py
									
									
									
									
									
								
							| @ -1,8 +1,9 @@ | ||||
| ''' | ||||
| CO3 is an abstract base class for scaffolding object hierarchies and managing operations | ||||
| with associated database schemas. It facilitates something like a "lightweight ORM" for | ||||
| classes/tables/states with fixed transformations of interest. The canonical use case is | ||||
| managing hierarchical document relations, format conversions, and syntactical components. | ||||
| CO3 is an abstract base class for scaffolding object hierarchies and managing | ||||
| operations with associated database schemas. It facilitates something like a | ||||
| "lightweight ORM" for classes/tables/states with fixed transformations of | ||||
| interest. The canonical use case is managing hierarchical document relations, | ||||
| format conversions, and syntactical components. | ||||
| 
 | ||||
| Generic collation syntax: | ||||
| 
 | ||||
| @ -22,12 +23,13 @@ Generic collation syntax: | ||||
| 
 | ||||
| .. admonition:: On multi-key attachment | ||||
| 
 | ||||
|     One possible quirk of the current collation registry scheme is the rather black and | ||||
|     white nature of key attachment. You either specify a single key, possibly to several | ||||
|     groups, or allow any key via passthrough under an implicit group. There's no explicit | ||||
|     "multi-key" pattern to make use of here, be it through "restricted passthrough" | ||||
|     (method still parameterized by the key, but only allows keys from a provided list) or | ||||
|     just simple duplicated attachment. To demonstrate via the above example: | ||||
|     One possible quirk of the current collation registry scheme is the rather | ||||
|     black and white nature of key attachment. You either specify a single key, | ||||
|     possibly to several groups, or allow any key via passthrough under an | ||||
|     implicit group. There's no explicit "multi-key" pattern to make use of | ||||
|     here, be it through "restricted passthrough" (method still parameterized by | ||||
|     the key, but only allows keys from a provided list) or just simple | ||||
|     duplicated attachment. To demonstrate via the above example: | ||||
| 
 | ||||
|     .. code-block:: python | ||||
| 
 | ||||
| @ -54,8 +56,8 @@ Generic collation syntax: | ||||
| 
 | ||||
|                 ... | ||||
| 
 | ||||
|     or with a central handler and separate collation points (at least when the key list is | ||||
|     small): | ||||
|     or with a central handler and separate collation points (at least when the | ||||
|     key list is small): | ||||
| 
 | ||||
|     .. code-block:: python | ||||
| 
 | ||||
| @ -71,38 +73,41 @@ Generic collation syntax: | ||||
|             def key2(self): | ||||
|                 self._handle_supported_keys('key2') | ||||
| 
 | ||||
|     The former scales better and allows general key rejection patterns if needed, while | ||||
|     the latter integrates a bit better with the formal collation process, e.g., will | ||||
|     throw ``ValueErrors`` based on key mismatches automatically. | ||||
|     The former scales better and allows general key rejection patterns if | ||||
|     needed, while the latter integrates a bit better with the formal collation | ||||
|     process, e.g., will throw ``ValueErrors`` based on key mismatches | ||||
|     automatically. | ||||
| ''' | ||||
| import inspect | ||||
| import logging | ||||
| from collections import defaultdict | ||||
| from functools import wraps, partial | ||||
| 
 | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def collate(key, groups=None): | ||||
|     ''' | ||||
|     Collation decorator for CO3 subtype action registry. | ||||
| 
 | ||||
|     Dynamic decorator; can be used as ``collate`` without any arguments, or with all. In | ||||
|     the former case, ``key`` will be a function, so we check for this. | ||||
|     Dynamic decorator; can be used as ``collate`` without any arguments, or | ||||
|     with all. In the former case, ``key`` will be a function, so we check for | ||||
|     this. | ||||
| 
 | ||||
|     .. admonition:: Usage | ||||
| 
 | ||||
|         Collation registration is the process of exposing various actions for use in | ||||
|         **hierarchical collection** (see ``Mapper.collect``). Collation *keys* are unique | ||||
|         identifiers of a particular action that emits data. Keys can belong to an arbitrary | ||||
|         number of *groups*, which serve as semantically meaningful collections of similar | ||||
|         actions. Group assignment also determines the associated *collation component* | ||||
|         to be used as a storage target; the results of actions $K_G$ belonging to group | ||||
|         $G$ will all be stored in the attached $G$-component. Specification of key-group | ||||
|         relations can be done in a few ways: | ||||
|         Collation registration is the process of exposing various actions for | ||||
|         use in **hierarchical collection** (see ``Mapper.collect``). Collation | ||||
|         *keys* are unique identifiers of a particular action that emits data. | ||||
|         Keys can belong to an arbitrary number of *groups*, which serve as | ||||
|         semantically meaningful collections of similar actions. Group | ||||
|         assignment also determines the associated *collation component* to be | ||||
|         used as a storage target; the results of actions $K_G$ belonging to | ||||
|         group $G$ will all be stored in the attached $G$-component. | ||||
|         Specification of key-group relations can be done in a few ways: | ||||
| 
 | ||||
|         - Explicit key-group specification: a specific key and associated groups can be | ||||
|           provided as arguments to the decorator: | ||||
|         - Explicit key-group specification: a specific key and associated | ||||
|           groups can be provided as arguments to the decorator: | ||||
| 
 | ||||
|           .. code-block:: python | ||||
| 
 | ||||
| @ -127,14 +132,14 @@ def collate(key, groups=None): | ||||
|                   ... | ||||
|               } | ||||
| 
 | ||||
|           If ``groups`` is left unspecified, the key will be attached to the default | ||||
|           ``None`` group. | ||||
|           If ``groups`` is left unspecified, the key will be attached to the | ||||
|           default ``None`` group. | ||||
| 
 | ||||
|         - Implicit key-group association: in some cases, you may want to support an entire | ||||
|           "action class," and associate any operations under the class to the same storage | ||||
|           component. Here we still use the notion of connecting groups to components, but | ||||
|           allow the key to be dynamically specified and passed through to the collation | ||||
|           method: | ||||
|         - Implicit key-group association: in some cases, you may want to | ||||
|           support an entire "action class," and associate any operations under | ||||
|           the class to the same storage component. Here we still use the notion | ||||
|           of connecting groups to components, but allow the key to be | ||||
|           dynamically specified and passed through to the collation method: | ||||
| 
 | ||||
|           .. code-block:: python | ||||
| 
 | ||||
| @ -160,27 +165,31 @@ def collate(key, groups=None): | ||||
| 
 | ||||
|           A few important notes: | ||||
| 
 | ||||
|           - Implicit key-group specifications attach the *group* to a single method, | ||||
|             whereas in the explicit case, groups can be affiliated with many keys. When | ||||
|             explicitly provided, only those exact key values are supported. But in the | ||||
|             implicit case, *any* key is allowed; the group still remains a proxy for the | ||||
|             entire action class, but without needing to map from specifically stored key | ||||
|             values. That is, the utility of the group remains consistent across implicit | ||||
|           - Implicit key-group specifications attach the *group* to a single | ||||
|             method, whereas in the explicit case, groups can be affiliated with | ||||
|             many keys. When explicitly provided, only those exact key values | ||||
|             are supported. But in the implicit case, *any* key is allowed; the | ||||
|             group still remains a proxy for the entire action class, but | ||||
|             without needing to map from specifically stored key values. That | ||||
|             is, the utility of the group remains consistent across implicit | ||||
|             and explicit cases, but stores the associations differently. | ||||
|           - The ``None`` key, rather than point to a ``(<method>, <group-list>)`` tuple, | ||||
|             instead points to a dictionary of ``group``-``method`` pairs. When attempting | ||||
|             execute a key under a particular group, the group registry indicates | ||||
|             whether the key is explicitly supported. If ``None`` is present for the group, | ||||
|             then ``key_registry[None][<group-name>]`` can be used to recover the method | ||||
|             implicitly affiliated with the key (along with any other key under the group). | ||||
|           - When any method has been implicitly registered, *any* key (even when | ||||
|             attempting to specify an explicit key) will match that group. This can | ||||
|             effectively mean keys are not unique when an implicit group has been | ||||
|             registered. There is a protection in place here, however; in methods like | ||||
|             ``CO3.collate`` and ``Mapper.collect``, an implicit group must be directly | ||||
|             named in order for a given key to be considered. That is, when attempting | ||||
|             collation outside specific group context, provided keys will only be | ||||
|             considered against explicitly registered keys. | ||||
|           - The ``None`` key, rather than point to a ``(<method>, | ||||
|             <group-list>)`` tuple, instead points to a dictionary of | ||||
|             ``group``-``method`` pairs. When attempting execute a key under a | ||||
|             particular group, the group registry indicates whether the key is | ||||
|             explicitly supported. If ``None`` is present for the group, then | ||||
|             ``key_registry[None][<group-name>]`` can be used to recover the | ||||
|             method implicitly affiliated with the key (along with any other key | ||||
|             under the group). | ||||
|           - When any method has been implicitly registered, *any* key (even | ||||
|             when attempting to specify an explicit key) will match that group. | ||||
|             This can effectively mean keys are not unique when an implicit | ||||
|             group has been registered. There is a protection in place here, | ||||
|             however; in methods like ``CO3.collate`` and ``Mapper.collect``, an | ||||
|             implicit group must be directly named in order for a given key to | ||||
|             be considered. That is, when attempting collation outside specific | ||||
|             group context, provided keys will only be considered against | ||||
|             explicitly registered keys. | ||||
|     ''' | ||||
|     func = None | ||||
|     if inspect.isfunction(key): | ||||
| @ -200,6 +209,7 @@ def collate(key, groups=None): | ||||
| 
 | ||||
|     return decorator | ||||
| 
 | ||||
| 
 | ||||
| class FormatRegistryMeta(type): | ||||
|     ''' | ||||
|     Metaclass handling collation registry at the class level. | ||||
| @ -225,8 +235,8 @@ class FormatRegistryMeta(type): | ||||
|                 for _, method in methods: | ||||
|                     register_action(method) | ||||
| 
 | ||||
|         # add final registered formats for the current class, overwriting any found in | ||||
|         # superclass chain | ||||
|         # add final registered formats for the current class, overwriting any | ||||
|         # found in superclass chain | ||||
|         for attr_name, attr_value in attrs.items(): | ||||
|             register_action(attr_value) | ||||
| 
 | ||||
| @ -235,41 +245,49 @@ class FormatRegistryMeta(type): | ||||
| 
 | ||||
|         return super().__new__(cls, name, bases, attrs) | ||||
| 
 | ||||
| 
 | ||||
| class CO3(metaclass=FormatRegistryMeta): | ||||
|     ''' | ||||
|     Base class supporting the central "COllate, COllect, COmpose" paradigm. | ||||
| 
 | ||||
|     - Collate: organize and transform conversion outputs, possibly across class components | ||||
|     - Collect: gather core attributes, conversion data, and subcomponents for DB insertion | ||||
|     - Compose: construct object-associated DB table references through the class hierarchy | ||||
|     - Collate: organize and transform conversion outputs, possibly across class | ||||
|       components | ||||
|     - Collect: gather core attributes, conversion data, and subcomponents for | ||||
|       DB insertion | ||||
|     - Compose: construct object-associated DB table references through the | ||||
|       class hierarchy | ||||
| 
 | ||||
|     .. admonition:: on action groups | ||||
| 
 | ||||
|         Group keys are simply named collections to make it easy for storage components to | ||||
|         be attached to action subsets. They do _not_ augment the action registration | ||||
|         namespace, meaning the action key should still be unique; the group key is purely | ||||
|         auxiliary. | ||||
|         Group keys are simply named collections to make it easy for storage | ||||
|         components to be attached to action subsets. They do _not_ augment the | ||||
|         action registration namespace, meaning the action key should still be | ||||
|         unique; the group key is purely auxiliary. | ||||
| 
 | ||||
|         Action methods can also be attached to several groups, in case there is | ||||
|         overlapping utility within or across schemas or storage media. In this case, it | ||||
|         becomes particularly critical to ensure registered ``collate`` methods really are | ||||
|         just "gathering results" from possibly heavy-duty operations, rather than | ||||
|         performing them when called, so as to reduce wasted computation. | ||||
|         overlapping utility within or across schemas or storage media. In this | ||||
|         case, it becomes particularly critical to ensure registered ``collate`` | ||||
|         methods really are just "gathering results" from possibly heavy-duty | ||||
|         operations, rather than performing them when called, so as to reduce | ||||
|         wasted computation. | ||||
| 
 | ||||
|     .. admonition:: New: collation caching | ||||
| 
 | ||||
|         To help facilitate the common pattern of storing collation results, a | ||||
|         ``collate_cache`` parameter has been added to store key-group indexed collation | ||||
|         results. (Note: now requires explicit superclass instantiation.) | ||||
|         ``collate_cache`` parameter has been added to store key-group indexed | ||||
|         collation results. (Note: now requires explicit superclass | ||||
|         instantiation.) | ||||
|     ''' | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self._collate_cache = {} | ||||
| 
 | ||||
|     @property | ||||
|     def attributes(self): | ||||
|         ''' | ||||
|         Method to define how a subtype's inserts should be handled under ``collect`` for | ||||
|         canonical attributes, i.e., inserts to the type's table. | ||||
|         Method to define how a subtype's inserts should be handled under | ||||
|         ``collect`` for canonical attributes, i.e., inserts to the type's | ||||
|         table. | ||||
|         ''' | ||||
|         return vars(self) | ||||
| 
 | ||||
| @ -284,14 +302,15 @@ class CO3(metaclass=FormatRegistryMeta): | ||||
|     def collation_attributes(self, key, group): | ||||
|         ''' | ||||
|         Return "connective" collation component data, possibly dependent on | ||||
|         instance-specific attributes and the action arguments. This is typically the | ||||
|         auxiliary structure that may be needed to attach to responses from registered | ||||
|         ``collate`` calls to complete inserts. | ||||
|         instance-specific attributes and the action arguments. This is | ||||
|         typically the auxiliary structure that may be needed to attach to | ||||
|         responses from registered ``collate`` calls to complete inserts. | ||||
| 
 | ||||
|         Note: this method is primarily used by ``Mapper.collect()``, and is called just | ||||
|         prior to collector send-off for collation inserts and injected alongside collation | ||||
|         data. Common structure in collation components can make this function easy to | ||||
|         define, independent of action group for instance. | ||||
|         Note: this method is primarily used by ``Mapper.collect()``, and is | ||||
|         called just prior to collector send-off for collation inserts and | ||||
|         injected alongside collation data. Common structure in collation | ||||
|         components can make this function easy to define, independent of action | ||||
|         group for instance. | ||||
|         ''' | ||||
|         return {} | ||||
| 
 | ||||
| @ -350,5 +369,3 @@ class CO3(metaclass=FormatRegistryMeta): | ||||
|             self._collate_cache[(key, group)] = result | ||||
| 
 | ||||
|         return result | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -11,12 +11,10 @@ logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| class Indexer: | ||||
|     ''' | ||||
|     Indexer class | ||||
|     Indexer base class | ||||
| 
 | ||||
|     Provides restricted access to an underlying Accessor to enable more efficient, superficial | ||||
|     caching. | ||||
| 
 | ||||
|     Cache clearing is to be handled by a wrapper class, like the Database. | ||||
|     caching. Note that cache clearing is to be handled by a wrapper class, like the Database. | ||||
| 
 | ||||
|     Caching occurs at the class level, with indexes prefixed by table's origin Composer. | ||||
|     This means that cached selects/group-bys will be available regardless of the provided | ||||
| @ -317,6 +315,7 @@ class Indexer: | ||||
| 
 | ||||
|         return list(group_by_idx.values()) | ||||
| 
 | ||||
| 
 | ||||
| class CacheBlock: | ||||
|     ''' | ||||
|     Wraps up a set of query parameters for a specific entity, and provides cached access | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| import logging | ||||
| from contextlib import contextmanager | ||||
| 
 | ||||
| from co3.engine import Engine, Connection, Resource, Group | ||||
| 
 | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| @ -10,11 +12,14 @@ class Medium[R: Resource]: | ||||
| 
 | ||||
|     A Resource space | ||||
|     ''' | ||||
|     def __init__(self, scope): | ||||
|     _engine_cls: type[Engine] = Engine | ||||
|   | ||||
|     def __init__(self, *scope_args, **scope_kwargs): | ||||
|         self.engine = self._engine_cls(*engine_args, **engine_kwargs) | ||||
|         pass | ||||
|          | ||||
|     @contextmanager | ||||
|     def connect(self, timeout=None): | ||||
|     def connect(self, timeout=None) -> Connection: | ||||
|         ''' | ||||
|         Open a connection to the database specified by the resource. Exactly what the | ||||
|         returned connection looks like remains relatively unconstrained given the wide | ||||
| @ -22,21 +27,59 @@ class Medium[R: Resource]: | ||||
|         with-statement contexts, constituting an "interaction session" with the database | ||||
|         (i.e., allowing several actions to be performed using the same connection). | ||||
|         ''' | ||||
|         raise NotImplementedError | ||||
|         return self.engine.connect(timeout=timeout) | ||||
| 
 | ||||
|     def execute(self, query: Query[QL]): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class BrowsableMedium[R: Resource](Medium[R]): | ||||
|     def browse(self, uri: URI[R]): | ||||
| class ReadableMedium[R: Resource](Medium[R]): | ||||
|     def _resolve_relative_uri(self, protocol, value): | ||||
|         ''' | ||||
|         Subclass to implement: fetch child object for supported protocol, i.e., single | ||||
|         component subpath | ||||
|         ''' | ||||
|         ... | ||||
| 
 | ||||
|     def resolve_uri(self, uri: URI) -> ResourceCollection: | ||||
|         assert uri.protocols[0] in self.supported_protocols | ||||
| 
 | ||||
|         obj = self._resolve_relative_uri(uri.protocols[0], uri.components[0]) | ||||
| 
 | ||||
|         # core the uri and recurse | ||||
|         cored_uri = uri.core(): | ||||
| 
 | ||||
|         if cored_uri: | ||||
|             rc = obj.resolve_uri(cored_uri) | ||||
|         else: | ||||
|             assert type(obj) is Resource | ||||
|             rc = ResourceCollection([obj]) | ||||
| 
 | ||||
|         return rc | ||||
| 
 | ||||
|     def _to_uri_list(self, uri_like): | ||||
|         if type(uri) is not list: | ||||
|             uri = [uri] | ||||
|         ... | ||||
| 
 | ||||
|     def browse( | ||||
|         self, | ||||
|         connection: Connection, | ||||
|         uri: str | URI | list[str] | list[URI] | ||||
|     ) -> ResourceCollection: | ||||
|         ''' | ||||
|         Analog for Read (CRUD), SELECT (SQL), GET (REST) | ||||
|         ''' | ||||
|         pass | ||||
|         uris = self._to_uri_list(uri) | ||||
| 
 | ||||
|         rc = ResourceCollection() | ||||
|         for uri in uris: | ||||
|             rc.extend(self._resolve_uri(uri)) | ||||
| 
 | ||||
|         return rc | ||||
| 
 | ||||
| 
 | ||||
| class ABCDMedium[R: Resource](BrowsableMedium[R]): | ||||
| class WritableMedium[R: Resource](ReadableMedium[R]): | ||||
|     def append(self, uri: URI[R], resource: R): | ||||
|         ''' | ||||
|         Analog for Create (CRUD), INSERT (SQL), POST/PUT (REST) | ||||
|  | ||||
							
								
								
									
										0
									
								
								co3/mediums/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								co3/mediums/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										6
									
								
								co3/mediums/disk.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								co3/mediums/disk.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| from co3 import Medium | ||||
| from co3.resources import INode | ||||
| 
 | ||||
| 
 | ||||
| class Disk[INode](Medium): | ||||
|     pass | ||||
| @ -1,10 +1,15 @@ | ||||
| from typing import Protocol | ||||
| from typing import BinaryIO | ||||
| 
 | ||||
| from co3 import URI, Medium | ||||
| 
 | ||||
| 
 | ||||
| class Resource: | ||||
|     def content(self) -> BinaryIO: | ||||
|         pass | ||||
|     def __init__( | ||||
|         self, | ||||
|         context: Medium | list[Medium], | ||||
|         uri: URI | ||||
|     ): | ||||
|         self.uri = uri | ||||
| 
 | ||||
| class SelectableResource(Protocol): | ||||
|     def select(self, component, *args, **kwargs): | ||||
|         raise NotImplementedError | ||||
|     def open(self) -> BinaryIO: | ||||
|         pass | ||||
|  | ||||
							
								
								
									
										5
									
								
								co3/resources/inode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								co3/resources/inode.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| from co3 import Resource | ||||
| 
 | ||||
| 
 | ||||
| class INode(Resource): | ||||
|     pass | ||||
							
								
								
									
										21
									
								
								co3/uri.py
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								co3/uri.py
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | ||||
| from urllib import parse | ||||
| from urllib.parse import urlparse | ||||
| 
 | ||||
| 
 | ||||
| class URI: | ||||
| @ -13,3 +13,22 @@ class URN(URI): | ||||
|     def __init__(self, url_str: str): | ||||
|         self.url_str = url_str | ||||
| 
 | ||||
| 
 | ||||
| class CompositeURI(URI): | ||||
|     def __init__(self, url_str: str): | ||||
|         url_obj = urlparse(url_str) | ||||
| 
 | ||||
|         self.protocols = url_obj.scheme.split('+')[::-1] | ||||
|         self.components = url_obj.scheme.split('+')[::-1] | ||||
| 
 | ||||
|     def core(self, layers=1) -> 'CompositeURI': | ||||
|         ''' | ||||
|         "Core" the innermost ``layers`` layers of the composite URI. | ||||
|         ''' | ||||
|         pass | ||||
| 
 | ||||
|     def shed(self, layers=1) -> 'CompositeURI': | ||||
|         ''' | ||||
|         "Shed" the outermost ``layers`` layers of the composite URI. | ||||
|         ''' | ||||
|         pass | ||||
|  | ||||
							
								
								
									
										11
									
								
								docs/reference/uri.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								docs/reference/uri.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| Quick thoughts and ideals: | ||||
| 
 | ||||
| - Schemes are compositional, "wrapping" super contexts: `c+b+a://a/b/c` | ||||
| - The scheme communicates the target type (above is `c`) | ||||
| - URIs can be arbitrarily relative so long as they're resolved in the right contexts. | ||||
|   Above, `c+b://b/c` can be resolved in the context of `a://a` | ||||
| - URIs are resolved by unwrapping schemes and resolving in to out | ||||
| - URL params can apply only to the target type (this is the most consistent and probably | ||||
|   not too restrictive) | ||||
| - Trajectories from one scheme to another can be inferred from the type hierarchy; there | ||||
|   may be many | ||||
							
								
								
									
										5
									
								
								tests/co3_medium_demo.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/co3_medium_demo.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| from co3.mediums import Disk | ||||
| 
 | ||||
| 
 | ||||
| disk = Disk('disk:///') | ||||
| disk.browse('dir://home') | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user