
    K:gL                        U d Z ddlZddlZddlZddlZddlZddlmZ ddlm	Z	m
Z
 ddlmZ ddlmZmZmZmZmZmZmZmZmZmZmZ ddlmZ ddlZddlmZmZ dd	lm Z  dd
l!m"Z"m#Z# ddl$m%Z% ddl&m'Z'm(Z(m)Z)m*Z*m+Z+m,Z,m-Z-m.Z.m/Z/m0Z0m1Z1m2Z2 ddl3m4Z4m5Z5 de6fdZ7d.de6dede6fdZ8	 	 	 	 	 	 	 	 d/de6de6de6dee#   deee9e jt                  f      de9de9de9de;fdZ<	 	 d0dee,   dee6   dede.fdZ=d1dede)fd Z>d2d!e6d"e6de(fd#Z?ee6e6f   Z@eeAd$<    G d% d&      ZBd'ee'e,f   de.fd(ZC	 d1d)ee   d*ee6ef   d+eee6      deed,f   fd-ZDy)3a*  
pyAirtable provides a number of helper functions for testing code that uses
the Airtable API. These functions are designed to be used with the standard
Python :mod:`unittest.mock` library, and can be used to create fake records,
users, and attachments, as well as to mock the Airtable API itself.
    N)defaultdict)	ExitStackcontextmanager)partialmethod)AnyDictIterableIteratorListOptionalSequenceTupleUnioncastoverload)mock)Self	TypeAlias)retrying)ApiTimeoutTuple)Table)AnyRecordDictAttachmentDictCollaboratorDictCreateRecordDict	FieldNameFieldsRecordDeletedDict
RecordDictRecordIdUpdateRecordDictUpsertResultDictWritableFields)fieldgetteris_airtable_idreturnc                  `    t         j                   j                         j                         dz   S )NZ)datetimenow	isoformat     O/var/www/html/lionshead/venv/lib/python3.12/site-packages/pyairtable/testing.py_nowr0   6   s%      ",,.44r.   typevaluec                     |Ddj                  t        j                  t        j                  t        j
                  z   d            }| |ddd z   S )a;  
    Generate a fake Airtable-style ID.

    Args:
        type: the object type prefix, defaults to "rec"
        value: any value to use as the ID, defaults to random letters and digits

    >>> fake_id()
    'rec...'
    >>> fake_id('tbl')
    'tbl...'
    >>> fake_id(value='12345')
    'rec00000000012345'
    N    z0>14)joinrandomsamplestringascii_lettersdigits)r1   r2   s     r/   fake_idr<   :   sG     }f&:&:V]]&JBOPU4L#2&&&r.   base_id
table_nameapi_keytimeoutretrytypecastuse_field_idsmemoizec           	      f    | xs t        d      |xs t        d      ||||||d}t        dd|      S )zJ
    Generate a ``Meta`` class for inclusion in a ``Model`` subclass.
    apptbl)r=   r>   r?   r@   rA   rB   rC   rD   Metar-   )r<   r1   )	r=   r>   r?   r@   rA   rB   rC   rD   attrss	            r/   	fake_metarJ   N   sF     ,gen 2GEN&	E E""r.   fieldsidother_fieldsc                 p    t        |d      rt        |      nt        |      t               i | xs i |dS )a  
    Generate a fake record dict with the given field values.

    >>> fake_record({"Name": "Alice"})
    {
        'id': '...',
        'createdTime': '...',
        'fields': {'name': 'Alice'}
    }

    >>> fake_record(name="Alice", id="123")
    {
        'id': 'rec00000000000123',
        'createdTime': '...',
        'fields': {'name': 'Alice'}
    }

    >>> fake_record(name="Alice", id="recABC00000000123")
    {
        'id': 'recABC00000000123',
        'createdTime': '...',
        'fields': {'name': 'Alice'}
    }
    rec)r2   rL   createdTimerK   )r&   strr<   r0   )rK   rL   rM   s      r/   fake_recordrS   h   s;    < (E2c"gb8Iv4fl4|4 r.   c                 |    t        d|       }|t        | xs |      j                          dt        | xs d      dS )z
    Generate a fake user dict with the given value for an email prefix.

    >>> fake_user("Alice")
    {
        'id': 'usr000000000Alice',
        'email': 'alice@example.com'
        'name': 'Alice'
    }
    usrz@example.comz	Fake User)rL   emailname)r<   rR   lower)r2   rL   s     r/   	fake_userrY      sG     
	B$**,-\:E([) r.   urlfilenamec                     |sEt         j                  j                  |       j                  xs dj	                  d      d   }|xs d}t        d      | xs d|dt        j                  |      d   xs d	d
S )a  
    Generate a fake attachment dict.

    >>> fake_attachment()
    {
        'id': 'att...',
        'url': 'https://example.com/',
        'filename': 'foo.txt',
        'size': 100,
        'type': 'text/plain',
    }

    >>> fake_attachment('https://example.com/image.png', 'foo.png')
    {
        'id': 'att...',
        'url': 'https://example.com/image.png',
        'filename': 'foo.png',
        'size': 100,
        'type': 'text/plain',
    }
    r4   /zfoo.txtattzhttps://example.com/d   r   z
text/plain)rL   rZ   r[   sizer1   )urllib3util	parse_urlpathsplitr<   	mimetypes
guess_type)rZ   r[   s     r/   fake_attachmentri      sx    , LL**3/44:AA#FrJ(yen,,$$X.q1A\ r.   BaseAndTableIdc            
          e Zd ZU dZg dZeeeeef   f   e	d<   e
e   e	d<   eeef   e	d<   d1dedd	fd
Zd2dZdefdZdedd	fdZededee   fd       Z eed      Z eed      Zedeeeef      dededee   fd       Zedeeeef      dedee   fd       Zdededee   fdZedeeeef      dededd	fd       Zedeeeef      dedd	fd       Zdededd	fdZd2dZ de!ded ededef
d!Z"ded"edeee      fd#Z#ded$ed"edefd%Z$ded&e%dedefd'Z&ded$ed(e'dedef
d)Z(ded$ede)fd*Z*dedee%   dedee   fd+Z+dedee,   dedee   fd,Z-ded-ee   dee)   fd.Z.dedee/   d/ee0   dede1f
d0Z2y	)3MockAirtablea  
    This class acts as a context manager which mocks several pyAirtable APIs,
    so that your tests can operate against tables without making network requests.

    .. code-block:: python

        from pyairtable import Api
        from pyairtable.testing import MockAirtable

        table = Api.base("baseId").table("tableName")

        with MockAirtable() as m:
            m.add_records(table, [{"Name": "Alice"}])
            records = table.all()
            assert len(table.all()) == 1

    If you use pytest, you might want to include this as a fixture.

    .. code-block:: python

        import pytest
        from pyairtable.testing import MockAirtable

        @pytest.fixture(autouse=True)
        def mock_airtable():
            with MockAirtable() as m:
                yield m

        def test_your_function():
            ...

    Not all API methods are supported; if your test calls a method that would
    make a network request, a RuntimeError will be raised instead.

        >>> with MockAirtable() as m:
        ...     table.schema()
        ...
        Traceback (most recent call last): ...
        RuntimeError: unhandled call to Api.request

    You can allow unhandled requests by setting the ``passthrough`` argument to True,
    either on the constructor or temporarily on the MockAirtable instance. This is
    useful when using another library, like `requests-mock <https://requests-mock.readthedocs.io/en/latest/>`_,
    to prepare responses for complex cases (like code that retrieves the schema).

    .. code-block:: python

        def test_your_function(requests_mock, mock_airtable, monkeypatch):
            base = Api.base("baseId")

            # load and cache our mock schema
            requests_mock.get(
                base.meta_url("tables"),
                json={"tables": [...]}
            )
            with mock_airtable.enable_passthrough():
                base.schema()

            # code below will fail if any more unhandled requests are made
            ...

    )
Api.requestzTable.iteratez	Table.getzTable.createzTable.updatezTable.deletezTable.batch_createzTable.batch_updatezTable.batch_deletezTable.batch_upsertrecords_stack_mocksFpassthroughr'   Nc                 2    || _         | j                          y)z
        Args:
            passthrough: if True, unmocked methods will still be allowed to
                perform real network requests. If False, they will raise an error.
        N)rq   _reset)selfrq   s     r/   __init__zMockAirtable.__init__  s     'r.   c                 H    d | _         i | _        t        t              | _        y N)ro   rp   r   dictrn   rt   s    r/   rs   zMockAirtable._reset!  s    "4(r.   c                    | j                   rt        d      t        t        j                  d      rt        d      | j                          t               | _         | j                  D ]w  }|j                  dd      j                         }t        | d| d       }t        j                  d| |d      x}| j                  |<   | j                   j                  |       y | S )	NzMockAirtable is not reentrantr   zMockAirtable cannot be nested._zpyairtable.T)side_effectautospec)ro   RuntimeErrorhasattrr   requestrs   r   mockedreplacerX   getattrr   patchrp   enter_context)rt   rW   side_effect_namer}   mocked_methods        r/   	__enter__zMockAirtable.__enter__&  s    ;;>??3;;'>??kKKD#||C5;;=!$!,<+=(>EK04

dV$'1 MDKK-
 KK%%m4   r.   exc_infoc                 P    | j                   r | j                   j                  |  y y rw   )ro   __exit__)rt   r   s     r/   r   zMockAirtable.__exit__:  s"    ;; DKK  (+ r.   allowedc              #   b   K   | j                   }|| _         	 |  || _         y# || _         w xY ww)a  
        Context manager that temporarily changes whether unmocked methods
        are allowed to perform real network requests. For convenience, there are
        also shortcuts ``enable_passthrough()`` and ``disable_passthrough()``.

        Usage:

            .. code-block:: python

                with MockAirtable() as m:
                    with m.enable_passthrough():
                        schema = base.schema()
                        hooks = table.webhooks()

                    # no more network requests allowed
                    ...

        Args:
            allowed: If ``True``, unmocked methods will be allowed to perform real
                network requests within this context manager. If ``False``,
                they will not be allowed.
        N)rq   )rt   r   originals      r/   set_passthroughzMockAirtable.set_passthrough>  s5     0 ##"	(J'DxDs   /# /	,/Tr=   table_id_or_namec                     y rw   r-   rt   r=   r   rn   s       r/   add_recordszMockAirtable.add_records`  s     r.   tablec                     y rw   r-   rt   r   rn   s      r/   r   zMockAirtable.add_recordsi  s     r.   argskwargsc                     t        ||dg      \  }}}|D cg c]  }t        |       }}| j                  ||f   j                  |D ci c]  }|d   |
 c}       |S c c}w c c}w )ar  
        Add a list of records to the mock Airtable instance. These will be returned
        from methods like :meth:`~pyairtable.Table.all` and :meth:`~pyairtable.Table.get`.

        Can be called with either a base ID and table name,
        or an instance of :class:`~pyairtable.Table`:

        .. code-block::

            m = MockAirtable()
            m.add_records("baseId", "tableName", [{"Name": "Alice"}])
            m.add_records(table, records=[{"id": "recFake", {"Name": "Alice"}}])

        .. note::

            The parameters to :meth:`~pyairtable.Table.all` are not supported by MockAirtable,
            and constraints like ``formula=`` and ``limit=`` will be ignored. It is assumed
            that you are adding records to specifically test a particular use case.
            MockAirtable is not a full in-memory replacement for the Airtable API.

        Args:
            base_id: |arg_base_id|
                *This must be the first positional argument.*
            table_id_or_name: |arg_table_id_or_name|
                This should be the same ID or name used in the code under test.
                *This must be the second positional argument.*
            table: An instance of :class:`~pyairtable.Table`.
                *This is an alternative to providing base and table IDs,
                and must be the first positional argument.*
            records: A sequence of :class:`~pyairtable.api.types.RecordDict`,
                :class:`~pyairtable.api.types.UpdateRecordDict`,
                :class:`~pyairtable.api.types.CreateRecordDict`,
                or :class:`~pyairtable.api.types.Fields`.
        rn   rL   )_extract_argscoerce_fake_recordrn   update)rt   r   r   r=   r>   rn   recordcoerceds           r/   r   zMockAirtable.add_recordsq  s}    F (5T6I;'O$W<CDG&%f-GDgz*+22078fVD\6!8	
 	 E8s   A	A$c                     y rw   r-   r   s       r/   set_recordszMockAirtable.set_records  s     r.   c                     y rw   r-   r   s      r/   r   zMockAirtable.set_records  s     r.   c                     t        ||dg      \  }}}| j                  ||f   j                          | j                  |||       y)a  
        Set the mock records for a particular base and table, replacing any existing records.
        See :meth:`~MockAirtable.add_records` for more information.

        Args:
            base_id: |arg_base_id|
                *This must be the first positional argument.*
            table_id_or_name: |arg_table_id_or_name|
                This should be the same ID or name used in the code under test.
                *This must be the second positional argument.*
            table: An instance of :class:`~pyairtable.Table`.
                *This is an alternative to providing base and table IDs,
                and must be the first positional argument.*
            records: A sequence of :class:`~pyairtable.api.types.RecordDict`,
                :class:`~pyairtable.api.types.UpdateRecordDict`,
                :class:`~pyairtable.api.types.CreateRecordDict`,
                or :class:`~pyairtable.api.types.Fields`.
        rn   )rn   N)r   rn   clearr   )rt   r   r   r=   r>   rn   s         r/   r   zMockAirtable.set_records  sJ    & (5T6I;'O$Wgz*+113*g>r.   c                 8    | j                   j                          y)zD
        Clear all records from the mock Airtable instance.
        N)rn   r   ry   s    r/   r   zMockAirtable.clear  s     	r.   apimethodrZ   c                 x    | j                   st        d      | j                  d   } |j                  |||fi |S )Nzunhandled call to Api.requestrm   )rq   r   rp   temp_original)rt   r   r   rZ   r   r   s         r/   _api_requestzMockAirtable._api_request  sA    >??]+#v##C???r.   optionsc                     t        | j                  |j                  j                  |j                  f   j                               gS rw   )listrn   baserL   rW   values)rt   r   r   s      r/   _table_iteratezMockAirtable._table_iterate  s3    T\\5::==%**"=>EEGHIIr.   	record_idc                 f    | j                   |j                  j                  |j                  f   |   S rw   )rn   r   rL   rW   )rt   r   r   r   s       r/   
_table_getzMockAirtable._table_get  s'    ||UZZ]]EJJ78CCr.   r   c                     | j                   |j                  j                  |j                  f   }t	        |      }|d   |v rt               |d<   |d   |v r|||d   <   |S )NrL   )rn   r   rL   rW   r   r<   )rt   r   r   r   rn   s        r/   _table_createzMockAirtable._table_create  se     ,,

uzz:;#F+Tlg%"9F4L Tlg% &tr.   rK   c                     | j                   |j                  j                  |j                  f   |   }|d   j	                  |       |S )NrK   )rn   r   rL   rW   r   )rt   r   r   rK   r   existss         r/   _table_updatezMockAirtable._table_update  s@     uzz}}ejj9:9Ex'r.   c                     | j                   |j                  j                  |j                  f   j	                  |       |ddS )NT)rL   deleted)rn   r   rL   rW   pop)rt   r   r   s      r/   _table_deletezMockAirtable._table_delete  s5    ejjmmUZZ0155i@D11r.   c                 L    |D cg c]  }| j                  ||       c}S c c}w rw   )r   rt   r   rn   r   r   s        r/   _table_batch_createz MockAirtable._table_batch_create  s+     AHHf""5&1HHH   !c           	      Z    |D cg c]  }| j                  ||d   |d          c}S c c}w )NrL   rK   )r   r   s        r/   _table_batch_updatez MockAirtable._table_batch_update  sA     "
! ufTlF84DE!
 	
 
s    (
record_idsc                 L    |D cg c]  }| j                  ||       c}S c c}w rw   )r   )rt   r   r   r   s       r/   _table_batch_deletez MockAirtable._table_batch_delete  s+    
 GQQj""5)4jQQQr   
key_fieldsc                    t        | }| j                  |j                  j                  |j                  f   }|j                         D ci c]  } ||      | }}g g g d}	|D ]  }
d|
v r_t        |
j                  d            }||   }|d   j                  |
d          |	d   j                  |       |	d   j                  |       f|j                   ||
            x}rC|d   j                  |
d          |	d   j                  |d          |	d   j                  |       | j                  ||
      }|	d   j                  |d          |	d   j                  |        |	S c c}w )zW
        Perform a batch upsert operation on the mocked records for the table.
        )updatedRecordscreatedRecordsrn   rL   rK   r   rn   r   )r%   rn   r   rL   rW   r   rR   getr   appendr   )rt   r   rn   r   r   keyexisting_by_idrexisting_by_keyresultr   r   existing_recordcreated_records                 r/   _table_batch_upsertz MockAirtable._table_batch_upsert  s|    :&uzz}}ejj&AB.<.C.C.EF.E3q619.EF  $
 Fv~

4 01	"0";)001AB'(//	:y!((9$3$7$7F$DDD)001AB'(//0EFy!((9!%!3!3E6!B'(//t0DEy!((8 " 1 Gs   
E()F)r'   N)3__name__
__module____qualname____doc__r   r   rj   r!   r    __annotations__r   r   rR   r   boolru   rs   r   r   r   r   r
   r   r   enable_passthroughdisable_passthroughr   r	   r   r   r   r   r   r   r   r   r   r   r   r$   r   r   r   r   r"   r   r   r   r   r#   r   r-   r.   r/   rl   rl      s   =@F .$x';"<<==YcND T )
4 (,# ,$ , (t ( ( (< '='?
 $sCx.)  
j	   $sCx.)	
 
j	 ( ( (Z8H (T 
 $sCx.)  
   $sCx.)	
 
 ? ? ? ?.@ @S @s @c @c @JE Jc Jd4
CS>T JD D# D# D* D ! 	
 
		 	 		
 	 
	25 2X 2BS 2II *+I 	I
 
j	I	
	
 *+	
 		

 
j		
RR X&R 
	 	R$$ -($ Y'	$
 $ 
$r.   rl   r   c                     d| vrdt        t        |       i} t        | j                  d      xs
 t	                     t        | j                  d      xs
 t                     | d   dS )a  
    Coerce a record dict or field mapping to the expected format for
    an Airtable record, creating a fake ID and createdTime if necessary.

    >>> coerce_fake_record({"Name": "Alice"})
    {'id': 'rec000...', 'createdTime': '...', 'fields': {'Name': 'Alice'}}
    rK   rL   rQ   rP   )r   r   rR   r   r<   r0   )r   s    r/   r   r   4  s_     vD01&**T"/gi06::m4>?" r.   r   r   extract.c                    |xs g }t               }t        j                         d   j                  }t	        | d         t
        u r.| d   j                  j                  | d   j                  g| dd } t        d | D              }|dd t        t        fk7  r't        | ddj                  d |D               d	      |D ].  }||v s|j                  |       g | |j                  |      } 0 |rt        | d
dj                  |             t        |       t        |      dz   k  rDt        |      t        |      k  r-t        |      |z
  }t        | ddj                  |             t        |       S )z
    Convenience function for functions/methods which accept either
    a Table or a (base_id, table_name) as their first posargs.
       r   Nc              3   2   K   | ]  }t        |        y wrw   )r1   ).0args     r/   	<genexpr>z _extract_args.<locals>.<genexpr>U  s     /$3T#Y$s      z  expected (str, str, ...), got (z, c              3   4   K   | ]  }|j                     y wrw   )r   )r   ts     r/   r   z _extract_args.<locals>.<genexpr>X  s     @^U]PQU]s   )z# got unexpected keyword arguments: z missing keyword arguments: )setinspectstackfunctionr1   r   r   rL   rW   tuplerR   	TypeErrorr6   addr   len)r   r   r   	extractedcallerargtypesextract_namemissings           r/   r   r   E  sw    mGI]]_Q((FDG}Qa9QR9/$//H|Sz!h6tyy@^U]@^7^6__`a
 	
  6!MM,'4T46::l34D  
 h9$))F:K9LM
 	
 4y3w<!##IW(Eg,*6(">tyy?Q>RSTT;r.   )rO   N)r4   r4   patFakePersonalAccessTokenNNTFF)NNrw   )r4   r4   )Er   r*   r   rg   r7   r9   collectionsr   
contextlibr   r   	functoolsr   typingr   r   r	   r
   r   r   r   r   r   r   r   unittestr   rb   typing_extensionsr   r   pyairtable.apir   pyairtable.api.apir   r   pyairtable.api.tabler   pyairtable.api.typesr   r   r   r   r   r   r   r    r!   r"   r#   r$   pyairtable.utilsr%   r&   rR   r0   r<   r   Retryr1   rJ   rS   rY   ri   rj   r   rl   r   r   r-   r.   r/   <module>r     s        # 0 #      - # 0 &    95c 5'# 'c 'S '* /&*37### # l#	#
 E$./0# # # # 
#6  $!V!! ! 	!HS $4 & S . D "#s(O	 +m m`u]F%:; 
 ( (,#
3-#cN# hsm$# 38_	#r.   