Currently (as implemented in #17414), timetables are serialised by their classes’ full import path. This works most of the time, but not in some cases, including:

* Nested in class or function
* Declared directly in a DAG file without a valid import name (e.g. `12345.py`)

It’s fundamentally impossible to fix some of the cases (e.g. function-local class declaration) due to how Python works, but by requiring the user to explicitly register the timetable class, we can at least expose that problem so users don’t attempt to do that.

However, since the timetable actually would work a lot of times without any additional mechanism, I’m also wondering if we should _require_ registration.

1. Always require registration. A DAG using an unregistered timetable class fails to serialise.
2. Only require registration when the timetable class has wonky import path. “Normal” classes work out of the box without registering, and user sees a serialisation error asking for registration otherwise.
3. Don’t require registration. If a class cannot be correctly serialised, tell the user we can’t do it and the timetable must be declared another way.
tests/serialization/test_dag_serialization.py
dag = DAG(dag_id='simple_dag', start_date=test_date)
        CustomOperator(task_id='simple_task', dag=dag, bash_command="true")
        serialized_dag = SerializedDAG.to_dict(dag)
        assert "bash_command" in serialized_dag["dag"]["tasks"][0]
        dag = SerializedDAG.from_dict(serialized_dag)
        simple_task = dag.task_dict["simple_task"]
        assert getattr(simple_task, "bash_command") == "true"
        
        
        
        
        assert serialized_dag["dag"]["tasks"][0]["_operator_extra_links"] == [
            {'tests.test_utils.mock_operators.CustomOpLink': {}}
        ]
        
        assert set(simple_task.extra_links) == {'Google Custom', 'airflow', 'github', 'google'}
        ti = TaskInstance(task=simple_task, execution_date=test_date)
        ti.xcom_push('search_query', "dummy_value_1")
        
        custom_inbuilt_link = simple_task.get_extra_links(test_date, CustomOpLink.name)
        assert 'http://google.com/custom_base_link?search=dummy_value_1' == custom_inbuilt_link
        
        google_link_from_plugin = simple_task.get_extra_links(test_date, GoogleLink.name)
        assert "https://www.google.com" == google_link_from_plugin
    def test_extra_operator_links_logs_error_for_non_registered_extra_links(self):
        """
        Assert OperatorLinks not registered via Plugins and if it is not an inbuilt Operator Link,
        it can still deserialize the DAG (does not error) but just logs an error
        """
        class TaskStateLink(BaseOperatorLink):